Использование графических данных и машинного обучения

В этом посте мы рассмотрим, как начать работу с практическими и масштабируемыми рекомендациями в виде графика. Мы рассмотрим фундаментальный пример с рекомендацией новостей для набора данных, содержащего 17,5 миллионов кликов и около 750 тысяч пользователей. Мы будем использовать Neo4j и библиотеку Graph Data Science (GDS), чтобы быстро предсказывать похожие новости на основе пользовательских предпочтений и включать рекомендательные запросы с точностью до секунды, упорядоченные по рангу, персонализированные для каждого пользователя.

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

Рекомендательные системы — богатая область для изучения, и мы лишь коснемся ее поверхности. Если вы хотите углубиться в какую-либо область — будь то другие методы рекомендаций, оценка производительности, оптимизация, включая дополнительные входные данные, способы дальнейшего масштабирования или любые другие рекомендации на основе графиков, оставьте примечание в комментариях.

Этот пост структурирован следующим образом: во-первых, мы кратко определим рекомендательные системы. Далее мы рассмотрим исходный набор данных и график, которые мы будем использовать, а также то, как запрашивать базовую статистику профилирования, чтобы помочь нам понять график и лучше подготовиться к анализу. Далее мы поговорим о методе под названием Collaborative Filtering (CF), который будет нашим механизмом рекомендаций в этом посте. После этого мы приступим к применению CF с использованием языка запросов Cypher и масштабированию с помощью библиотеки Graph Data Science (GDS), используя встраивание узлов и технику машинного обучения, называемую K-Nearest Neighbor (KNN). Наконец, мы поговорим о следующих шагах и последующих ресурсах.

Что такое рекомендательные системы?

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

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

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

Сегодняшний набор данных: Microsoft MIND

В этом посте мы рассмотрим рекомендацию новостей с большим набором данных MIcrosoft Nnews Dataset (MIND), который представляет собой образец из 1 миллионов анонимных пользователей и информацию об их поведении, собранную с веб-сайта Microsoft News [1]. Он включает в себя около 15 миллионов журналов показов для около 160 тысяч новостных статей на английском языке.

Я отформатировал набор данных и загрузил его в график со следующей схемой:

//visualize schema in Neo4j Browser
neo4j$ CALL db.schema.visualization();

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

Кроме того, у новостей также есть подкатегории и категории, которые были аннотированы Microsoft, а также объекты WikiData, которые Microsoft извлекла из заголовков и рефератов новостей с помощью методов НЛП. Я также смоделировал их с помощью узлов и отношений на графике. Я кратко коснусь их в конце, и они будут полезны для расширенного анализа, но мы не будем их использовать в этом посте. Наш основной интерес здесь — начать работу с пользователями и событиями их кликов, чтобы получить рекомендации.

Технический анализ

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

Профилирование графических данных

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

Совокупный подсчет

Начнем с большого количества узлов и связей. Эти функции предполагают, что APOC установлен на вашей базе данных Neo4j.

gds = GraphDataScience(HOST, auth=(USERNAME, PASSWORD), aura_ds=True)
# total node counts
gds.run_cypher( '''
    CALL apoc.meta.stats()
    YIELD labels AS nodeCounts
    UNWIND keys(nodeCounts) AS label
    WITH label, nodeCounts[label] AS nodeCount
    WHERE label IN ['User','News']
    RETURN label, nodeCount
''')

# total relationship counts
gds.run_cypher( '''
    CALL apoc.meta.stats()
    YIELD relTypesCount as relationshipCounts
    UNWIND keys(relationshipCounts) AS type
    WITH type, relationshipCounts[type] AS relationshipCount
    WHERE type IN ['CLICKED','HISTORICALLY_CLICKED']
    RETURN type, relationshipCount
''')

Что касается рекомендательных систем, то это график относительно небольшого размера, всего около 750 000 пользователей, 100 000 новостных статей и около 17,5 млн кликов. Эти цифры меньше, чем общие данные, указанные для MIND Large, потому что часть исходного набора данных была отложена Microsoft в качестве тестового набора для оценки конкурентов и, следовательно, не содержит полной информации о впечатлениях. Эта часть данных была исключена из этого графика.

Щелкните Рассылки событий.

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

  1. Граф достаточно хорошо связан, так как качество нашей будущей методики рекомендаций будет зависеть от достаточно хорошо связанного графа.
  2. У нас нет больших суперузлов, то есть узлов с очень большим количеством связей. То, что квалифицируется как супернода, сильно зависит от варианта использования. Для этого я бы беспокоился о пользователях с десятками тысяч кликов.
all_clicks_df = degree_counts('User', 'CLICKED|HISTORICALLY_CLICKED', 'OUT')
recent_clicks_df = degree_counts('User', 'CLICKED', 'OUT')
f, axs = plt.subplots(1,2,figsize=(16,5))
axs[0].bar(all_clicks_df.degree[:80], all_clicks_df.degreeCount[:80], width=1)
axs[0].set_title('User Clicks Distribution')
axs[0].set_ylabel('User Count')
axs[0].set_xlabel('Number of Total Clicks')
plt.figtext(0.4, 0.5, get_percentiles(all_clicks_df).to_string())
axs[1].bar(recent_clicks_df.degree[:80], recent_clicks_df.degreeCount[:80], width=1)
axs[1].set_title('User Recent Clicks Distribution')
axs[1].set_ylabel('User Count')
axs[1].set_xlabel('Number of Recent Clicks')
plt.figtext(0.83, 0.5, get_percentiles(recent_clicks_df).to_string())
plt.show()

На графике выше показано распределение общего количества кликов (CLICKED и HISTORICALLY_CLICKED), а также последних кликов (всего CLICKED) по пользователям. Мы видим, что распределения имеют тяжелые левые хвосты, показывающие, что активность неравномерно распределена среди пользователей — скорее, существует относительно небольшая доля пользователей, на которые приходится большое количество кликов. Это несколько ожидаемо. Важно отметить, что у каждого пользователя есть хотя бы одно недавнее событие клика, и мы не видим пользователей с более чем десятками тысяч событий клика.

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

all_clicks_df = degree_counts('News', 'CLICKED|HISTORICALLY_CLICKED', 'IN')
recent_clicks_df = degree_counts('News', 'CLICKED', 'IN')
f, axs = plt.subplots(1,2,figsize=(16,5))
axs[0].bar(all_clicks_df.degree[:80], all_clicks_df.degreeCount[:80], width=1)
axs[0].set_title('News Total Clicks Distribution')
axs[0].set_ylabel('News Count')
axs[0].set_xlabel('Number of Total Clicks')
plt.figtext(0.4, 0.5, get_percentiles(all_clicks_df).to_string())
axs[1].bar(recent_clicks_df.degree[:80], recent_clicks_df.degreeCount[:80], width=1)
axs[1].set_title('News Recent Clicks Distribution')
axs[1].set_ylabel('News Count')
axs[1].set_xlabel('Number of Recent Clicks')
plt.figtext(0.83, 0.5, get_percentiles(recent_clicks_df).to_string())
plt.show()

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

Маркировка последних новостей

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

  1. Новости имеют тенденцию быть наиболее актуальными, когда они недавние, и могут быстро потерять актуальность с течением времени. Как правило, рекомендующим хорошие новости следует избегать неактуальных и/или устаревших новостей. В этом случае мы можем использовать показы Microsoft в качестве прокси и рассматривать только новостные статьи, которые были включены в показ в пределах нашего временного окна выборки. Для этого мы можем использовать атрибут узла с именем approxTime, который отражает минимальное время показа новостной статьи. Этот атрибут я вычислял при загрузке исходных данных в Neo4j. Короче говоря, approxTime будет ненулевым только для новостных статей с хотя бы одним показом в нашей выборке.
  2. Ограничение CF заключается в том, что он может рекомендовать контент только с учетом отзывов пользователей. Таким образом, новостные статьи без кликов пользователей здесь не могут использоваться для CF. В приложениях это иногда называют проблемой холодного старта. Эту проблему можно решить с помощью рекомендаций на основе контента и других гибридных подходов. Мы можем рассказать об этом в отдельной публикации, но в данном примере мы рассматриваем только новостные статьи, на которые кликнул хотя бы один пользователь, т. е. хотя бы одно отношение CLICKED или HISTORICALLY_CLICKED.

Мы можем назначить вторую метку узла, RecentNews, чтобы позволить нам легко фильтровать новости, которые соответствуют вышеуказанным критериям в запросах Cypher и прогнозах GDS. Помните, что Neo4j позволяет узлу иметь несколько меток, поэтому исходная метка News все равно будет сохранена.

gds.run_cypher('''
    MATCH(n:News)<-[:CLICKED|HISTORICALLY_CLICKED]-()
    WHERE n.approxTime IS NOT NULL
    SET n:RecentNews
    RETURN count(DISTINCT n)
''')

Мы видим значительное сокращение количества новостных статей, со 104 000 до примерно 22 000. Но, будучи более свежими и хорошо связанными, эти новости также, вероятно, будут более актуальными, поэтому мы должны улучшить наш рекомендатель, сузив фокус до этих новостных статей. Мы также можем увидеть эти улучшения, отраженные в распределении кликов.

all_clicks_df = degree_counts('RecentNews', 'CLICKED|HISTORICALLY_CLICKED', 'IN')
recent_clicks_df = degree_counts('RecentNews', 'CLICKED', 'IN')

f, axs = plt.subplots(1,2,figsize=(16,5))

axs[0].bar(all_clicks_df.degree[:80], all_clicks_df.degreeCount[:80], width=1)
axs[0].set_title('Total Clicks Distribution')
axs[0].set_ylabel('News Count')
axs[0].set_xlabel('Number of Total Clicks')
plt.figtext(0.4, 0.5, get_percentiles(all_clicks_df).to_string())


axs[1].bar(recent_clicks_df.degree[:80], recent_clicks_df.degreeCount[:80], width=1)
axs[1].set_title('Recent Clicks Distribution')
axs[1].set_ylabel('News Count')
axs[1].set_xlabel('Number of Recent Clicks')
plt.figtext(0.83, 0.5, get_percentiles(recent_clicks_df).to_string())

plt.show()

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

Совместная фильтрация (CF)

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

CF на основе пользователей и элементов

Грубо говоря, существует два основных класса совместной фильтрации:

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

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

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

Неявная совместная фильтрация

Обычно вы слышите о совместной фильтрации в контексте явных отзывов пользователей, таких как обзоры фильмов или продуктов в интернет-магазине. В этом посте мы будем применять совместную фильтрацию только к активности кликов по новостям, то есть 1 для клика, подразумевая, что пользователю «нравится» часть контента, и 0 в противном случае. Это базовая форма «неявной совместной фильтрации», когда предпочтения пользователя подразумеваются на основе их действий, а не в явной обратной связи. Потребность в неявном CF возникает часто, поскольку во многих сценариях реального мира не будет явных рейтингов, а если и будут, то они будут очень редко заполнены. В производственных условиях вы также можете включить другие точки данных об активности пользователей, чтобы помочь взвесить отношения графика, такие как время чтения/просмотра, глубина прокрутки и т. д., которые не являются общедоступными в наборе данных MIND. Чем больше у вас информации, тем точнее вы сможете моделировать предпочтения пользователей.

Базовые шифровальные запросы для CF

Отсюда мы могли бы попробовать просто использовать Cypher для выполнения базовой совместной фильтрации. Например, возьмем приведенного ниже пользователя и новости, на которые он нажал. Вы можете увидеть смешанный интерес между автомобилями, финансами, новостями США и парой других категорий.

USER_ID = 'U218584'
gds.run_cypher('''
    MATCH (u1:User {userId: $userId})
           -[r1:CLICKED|HISTORICALLY_CLICKED]->(n1:RecentNews)
    RETURN n1.newsId AS newsId,
           n1.title AS title,
           n1.abstract AS abstract,
           n1.category AS category,
           n1.subcategory As subcategory,
           r1.impressionTime AS impressionTime,
           type(r1) AS clickType
    ORDER BY clickType, impressionTime DESC
    ''', params={'userId': USER_ID})

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

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

gds.run_cypher('''
    MATCH (u1:User {userId: $userId})
           -[r1:CLICKED]->(n1:RecentNews)
           <-[r2:CLICKED]-(u2:User)
           -[r3:CLICKED]->(n2:RecentNews)
    RETURN u1.userId AS userId,
           count(DISTINCT n1) AS clickedNews,
           count(DISTINCT u2) AS likeUsers,
           count(DISTINCT n2) AS potentialRecommendations
    ''', params={'userId': USER_ID})

Хотя описанное выше может хорошо работать в некоторых случаях, и хотя это, безусловно, может быть огромным улучшением по сравнению с объединением таблиц SQL или перекрестным обходом хранилищ документов, обратите внимание, что мы получаем много потенциальных рекомендаций (почти 11 КБ) и должны пройти множество пользовательских узлов (более 63 тыс.). И это всего лишь образец всего набора данных Microsoft, и мы вообще не используем HISTORICALLY CLICKED информацию.

Для производственных случаев использования, когда рекомендации необходимо будет часто запрашивать, этот метод будет иметь проблемы с масштабированием по мере роста числа пользователей, объема контента и/или наблюдаемого взаимодействия. Нам нужна другая стратегия, которая поможет сузить результаты. Есть несколько разных способов сделать это, но один надежный и масштабируемый способ сделать это — это библиотека Neo4j Graph Data Science (GDS) Library.

Масштабирование CF с помощью встраивания узлов FastRP и KNN

С помощью GDS мы можем использовать вложения узлов FastRP, чтобы уменьшить размерность проблемы, а затем использовать технику неконтролируемого машинного обучения под названием K-ближайший сосед (KNN) для определения и составления рекомендаций между новостями с похожими/близкими вложениями. Поскольку встраивания FastRP основаны на структуре графа, новости с похожими вложениями также должны быть относительно связаны в графе, поскольку на них нажимают одни и те же и похожие пользователи.

Графическая проекция

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

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

# Putting an index on a 'weight' attribute allows us to assign default values in the projection below
gds.run_cypher('''
   CREATE INDEX weight_index IF NOT EXISTS FOR ()-[r:CLICKED]-() 
   ON (r.weight)
''')
# Projecting the graph
g0, res = gds.graph.project('embedding-projection', ['User', 'News'], {
    'CLICKED':{
        'orientation':'UNDIRECTED',
        'properties': {
            'weight': {
                'property': 'confidence', 
                'defaultValue': 1.0
            }
        }
    },
    'HISTORICALLY_CLICKED':{
        'orientation':'UNDIRECTED',
        'properties': {
            'weight': {
                'property': 'confidence', 
                'defaultValue': 0.1
            }
        }
    }
})
res

FastRP

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

gds.fastRP.mutate(g0, mutateProperty='embedding', 
                  embeddingDimension=256, randomSeed=7474, 
                  relationshipWeightProperty='weight');

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

gds.graph.writeNodeProperties(g0, ["embedding"], ["News"])

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

gds.run_cypher('''
    MATCH(n:RecentNews) 
    RETURN n.newsId, n.embedding LIMIT 3
''')

K-ближайшие соседи (KNN)

Теперь мы можем запустить KNN, чтобы оценить отношения сходства (также известные как USERS_ALSO_LIKED) между статьями RecentNews и записать их обратно на график.

#graph projection for knn
g1, res = gds.graph.project('cf-projection', 
        {'RecentNews':{'properties':['embedding']}},'*')
res

#run knn
knn_stats_df = gds.knn.write(g1, nodeProperties=['embedding'],
    writeRelationshipType='USERS_ALSO_LIKED',
    writeProperty='score',
    sampleRate=1.0,
    maxIterations=1000);

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

knn_stats_df[['didConverge',
              'ranIterations',
              'computeMillis',
              'writeMillis',
              'nodesCompared',
              'nodePairsConsidered',
              'relationshipsWritten']]

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

knn_stats_df.similarityDistribution

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

CF с отношениями KNN

Теперь мы можем структурировать запрос совместной фильтрации для пользователя U218584, но с:

  1. более точные результаты,
  2. с использованием меньшего количества шагов обхода и
  3. с оценкой от KNN, которая позволяет нам упорядочивать результаты на основе совокупного сходства.
gds.run_cypher( '''
    MATCH(u:User {userId: $userId})-[:CLICKED]->(n:RecentNews)
    WITH collect(id(n)) AS clickedNewsIds
//get similar News according to KNN and exclude previously clicked news
    MATCH (clickedNews)-[s:USERS_ALSO_LIKED]->(similarNews:News)
    WHERE id(clickedNews) IN clickedNewsIds AND NOT id(similarNews) IN clickedNewsIds
//aggregate and return ranked results
    RETURN DISTINCT similarNews.newsId as newsId, 
        similarNews.title AS title, 
        similarNews.category AS category,
        similarNews.subcategory As subcategory,
        sum(s.score) AS totalScore ORDER BY totalScore DESC
    ''', params={'userId': USER_ID})

Мы сократили результаты до 59 с предыдущих почти 11 тысяч с чистым Cypher.

Рекомендации на основе последнего просмотренного контента

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

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

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

gds.run_cypher('''
    MATCH (u:User {userId:$userId})-[r:CLICKED]->(:RecentNews) 
    WITH u, max(r.impressionTime) AS maxImpressionTime
    MATCH (u)-[r:CLICKED]->(n:RecentNews) 
    WHERE r.impressionTime = maxImpressionTime
    RETURN n.newsId as newsId, 
        n.title AS title, 
        n.category AS category,
        n.subcategory As subcategory,
        r.impressionTime AS impressionTime
    ''', params={'userId': USER_ID})

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

gds.run_cypher('''
    MATCH (u:User {userId:$userId})-[r:CLICKED]->(:RecentNews) 
    WITH u, max(r.impressionTime) AS maxImpressionTime
    MATCH (u)-[r:CLICKED]->(n:RecentNews) 
    WHERE r.impressionTime = maxImpressionTime
    WITH n
    MATCH(n)-[s:USERS_ALSO_LIKED]->(similarNews:News)
    RETURN DISTINCT similarNews.newsId as newsId,
        similarNews.title AS title,
        similarNews.abstract AS abstract,
        similarNews.category AS category,
        similarNews.subcategory As subcategory,
        sum(s.score) AS totalScore
        ORDER BY totalScore DESC
    ''', params = {'userId': USER_ID})

Обратите внимание, что они более узко ориентированы на автомобили и, в частности, на Ford GT500.

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

Что дальше?

В этом посте мы рассмотрели, как мы можем начать быстро разрабатывать и масштабировать рекомендации с помощью надежной аналитики графов с использованием Neo4j и GDS.

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

Если вы новичок в Neo4j, я рекомендую для этого примера использовать Neo4j Desktop с GDS. Для совместимости используйте GDS версии 2.0 или выше.

Как упоминалось во введении, в этом посте мы едва касаемся поверхности рекомендательных систем на основе графов. Просто в CF есть много разных способов, которыми мы могли бы расширить наш анализ, а также различные способы, которыми мы могли бы оптимизировать его для повышения производительности прогнозирования, например, с оптимизацией FastRP, показанной здесь. Мы также даже не коснулись фильтрации на основе контента и других гибридных рекомендательных систем, которые могут быть особенно полезны для повышения производительности и рекомендации нового контента или контента с редкими отзывами пользователей. Это можно изучить с помощью текущего графика, используя узлы категории, подкатегории и wikiData в дополнение к другим атрибутам новостной статьи. Как правильно оценивать рекомендательные системы — тоже нетривиальная тема, которую стоит рассмотреть в контексте графа. Если вы заинтересованы в более подробном изучении этих или других тем, связанных с графическими рекомендациями, сообщите мне об этом в комментариях.

До тех пор я надеюсь, что эта демонстрация графика для рекомендательных систем была полезной!

Благодарим CJ Sullivan и Angela Zimmerman, которые помогли просмотреть содержание этой записи в блоге.

[1]: Фанчжао Ву, Ин Цяо, Цзюнь-Хунг Чен, Чухан Ву, Тао Ци, Цзяньсюнь Лянь, Даньян Лю, Син Се, Цзяньфэн Гао, Винни Ву и Мин Чжоу. MIND: крупномасштабный набор данных для рекомендаций по новостям. АКЛ 2020.