3 月 19 日下午 2 点,锁定 NVIDIA AI 网络中文专场。立即注册观看
数据科学

使用 NetworkX、Jaccard Similarity 和 cuGraph 预测您下一部最喜欢的电影

随着全球每个人可用的数据量不断增加,消费者做出明智决策的能力也越来越难。幸运的是,对于推荐系统而言,大型数据集是一个非常有用的组件,有时这会让某些情况下的决策变得更加容易。

对于为推荐系统提供支持的数据中固有的关系建模而言,图形是一个很好的选择,而 NetworkX 是许多数据科学家在 Python 中进行图形分析时非常喜欢的选择。NetworkX 易于学习和使用,拥有各种图形算法,并由庞大而友好的社区提供支持,并且在 Notebook、文档、Stack Overflow 和您喜欢的 LLM 中提供了大量示例。然而,令无数开发人员失望的是,他们使用 NetworkX 或甚至因为 NetworkX 而涉足图形分析,但众所周知,它在典型推荐系统使用的规模上的性能表现不佳。

这就引出了一个问题:能否用 Python 的几行简单代码编写有效的基于图形的推荐系统?更一般地说,开发者和数据科学家能否同时进行易于使用的高性能图形分析?

这两个问题的答案都是“Yes”

请继续阅读,了解如何使用 NetworkX、Jaccard Similarity 算法和 NVIDIA cuGraph 后端 (可将现代大规模图形数据所需的速度提高 250 倍以上),在 Python 中创建简单有效的推荐系统,使用 3300 万条电影评论的数据集。

MovieLens 数据集 

我们先从系统中最重要的部分开始:数据。MovieLens 数据集 1 可供公开 下载 README 文件 中有更详细的说明。该系列包括大约 331k 匿名用户,他们在观看 87k 部电影,获得了 34M 的评分。

Image showing how the MovieLens data can be shown as a graph.
图 1、MovieLens 数据可以表示为一个图形,其中各个评分可轻松映射到用户和电影节点之间的边缘。

从数据中提取建议:二分图和 Jaccard Similarity

我们根据 MovieLens 数据创建的图形类型是二部图,因为只有两种类型的节点:电影节点和用户节点,并且评论(边缘)只能在用户和电影之间发生。这使得应用 Jaccard Similarity 算法来查找电影之间的相似性变得特别容易。Jaccard Similarity 比较节点对,并使用它们在图形中的关系计算相似性系数。在这种情况下,电影根据用户选择的观看和审查方式相互关联。

Image showing how the  Jaccard Similarity algorithm can be used to find similarities between movies.
图 3、Jaccard Similarity 使用所比较的两个节点的近邻集的大小来计算相似性系数。根据用户的观看偏好,我们可以看到 m3 与 m2 更相似,电影 m4 和 m1 完全不相似。此系统会向喜欢 m3 的用户推荐 m2,而不会向喜欢 m4 的用户推荐 m1。

NetworkX 可以轻松处理较小的图形

不足为奇的是,NetworkX 支持我们上述的分析类型,而且只需使用几行 Python 代码即可轻松查看结果。但正如我们将看到的,当使用无 GPU 加速的 cuGraph 后端的 NetworkX 时,大型图形 (例如我们的电影推荐系统所需的图形) 的性能会受到限制。

我们将在下方查看推荐系统的关键部分,但可在 此处 获取完整源代码。

由于我们使用的 Jaccard Similarity 算法未考虑边缘权重,因此会将所有评论视为相同。我们不希望推荐好评度低的电影,因此我们会过滤掉某个值下的所有好评度,这会导致图形也变小。

# Create a separate DataFrame containing only "good" reviews (rating >= 3).
good_ratings_df = ratings_df[ratings_df["rating"] >= 3]
good_user_ids = good_ratings_df["userId"].unique()
good_movie_ids = good_ratings_df["movieId"].unique()

如果我们打印正在处理的数据的大小,我们会看到好评图大约有 330k 个节点,28M 个边缘,平均度数 (每个节点的近邻数) 为 84:

total number of users: 330975
total number of reviews: 33832162
average number of total reviews/user: 102.22
total number of users with good ratings: 329127
total number of good reviews: 27782577
average number of good reviews/user: 84.41

如上所述,这种规模的图形通常会给 NetworkX 带来挑战,但使用 cuGraph 后端的 GPU 加速消除了通常与如此多的数据相关的性能限制。不过,我们将继续使用 CPU 环境来演示默认性能。

注意 以下所有示例均在使用 NetworkX 3.4.2 和 Intel(R) Xeon(R) Platinum 8480CL@2.0GHz(2TB RAM)的工作站上运行

使用由用户创建的 NetworkX 图形和优秀的电影评论,我们来选择一个用户,找到他们评分最高的电影之一,并使用 Jaccard Similarity 找到类似的其他电影。

# Pick a user and one of their highly-rated movies
user = good_user_ids[321]
user_reviews = good_user_movie_G[user]
highest_rated_movie = max(
	user_reviews,
	key=lambda n: user_reviews[n].get("rating", 0)
)

当我们在电影名称贴图中查找节点 ID 时,我们会发现该用户评分最高的电影之一是动画电影“Mulan”:

highest rated movie for user=289308 is Mulan (1998), id: 1907, rated: {'rating': 5.0}

我们现在可以使用 Jaccard Similarity 根据用户的偏好和观看历史记录来推荐电影:

%%time
# Run Jaccard Similarity
jacc_coeffs = list(nx.jaccard_coefficient(good_user_movie_G, ebunch))

CPU times: user 2min 5s, sys: 15.4 ms, total: 2min 5s
Wall time: 2min 14s

使用默认 NetworkX 实现的 Jaccard 相似性计算运行了两分钟以上。根据这些结果,我们现在可以提供推荐。

# Sort by coefficient value, which is the 3rd item in the tuples
jacc_coeffs.sort(key=lambda t: t[2], reverse=True)
 
# Create a list of recommendations ordered by "best" to "worst" based on the
# Jaccard Similarity coefficients and the movies already seen
movies_seen = list(good_user_movie_G.neighbors(user))
recommendations = [mid for (_, mid, _) in jacc_coeffs
               	  if mid not in movies_seen]

现在,我们只需在已排序的推荐列表中打印出第一部电影:

User ID 289308 might like Tarzan (1999) (movie ID: 2687)

代码很简单,结果看起来不错,但性能却拖累了我们

如我们所见,这个推荐似乎是合理的;喜欢“Mulan”的人似乎也喜欢 1999 年的迪士尼动画电影“Tarzan”。

但是,如果我们的目标是提供服务,或分析数百乃至数千部电影,那么两分钟的运行时间就能让我们开始寻找 NetworkX 的替代方案。我们可以看到,使用此系统查找其他电影相似点的速度并没有加快:

%%time
# 1196: "Star Wars: Episode V - The Empire Strikes Back (1980)"
print_similar_movies(1196)

movies similar to Star Wars: Episode V - The Empire Strikes Back (1980):
movieId=260, Star Wars: Episode IV - A New Hope (1977)
movieId=1210, Star Wars: Episode VI - Return of the Jedi (1983)
movieId=1198, Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981)
CPU times: user 13min 47s, sys: 71.8 ms, total: 13min 47s
Wall time: 11min 30s

%%time
# 318: "Shawshank Redemption, The (1994)"
print_similar_movies(318) 

movies similar to "Shawshank Redemption, The (1994)":
movieId=296, Pulp Fiction (1994)
movieId=593, "Silence of the Lambs, The (1991)"
movieId=356, Forrest Gump (1994)
CPU times: user 28min 28s, sys: 172 ms, total: 28min 28s
Wall time: 16min 49s

鉴于此系统仅由几行代码组成,因此所返回的推荐内容的质量令人印象深刻。但是,运行时性能使其几乎无法使用。如上所述,根据“Shawshank Redemption, The (1994)”查找推荐内容大约需要 17 分钟。

NVIDIA cuGraph 使其变革性地加快

上述工作流程中的图形算法成本高昂,但通过使用 NVIDIA cuGraph 后端和兼容的 GPU,我们可以在不更改代码的情况下显著提高性能。

nx-cugraph 版本 25.02 或更高版本支持 Jaccard Similarity。版本 25.02 可在 nightly builds 中使用,并将于本月晚些时候纳入未来的稳定版本中。有关如何使用 conda 或 pip 从 nightly 和 stable 通道安装 nx-cugraph 以及其他 RAPIDS 包的说明,请参阅 RAPIDS Installation Guide

安装后,只需设置环境变量即可启用 nx-cugraph:

NX_CUGRAPH_AUTOCONFIG=True

cuGraph 利用 GPU 显著加速近邻查找,并设置 Jaccard 相似性计算所需的比较结果。此外,随着图形规模以及每部电影的电影和评论数量的增加,性能几乎保持不变。

该系统最优秀的部分,即代码的简单性,并没有改变,结果也是一样的,但在过去近 17 分钟的运行中,性能提高了 250 倍以上,缩短到 4 秒以内。

Barchart showing how Jaccard Similarity speedup with cuGraph over NetworkX for Movie Recommendations.
图 4、图表显示了各种电影的 cuGraph over NetworkX for Jaccard Similarity 计算速度 软件:NetworkX 3.4.2,cuGraph/nx-cugraph 25.02 CPU:Intel(R) Xeon(R) Platinum 8480CL@2.0GHz 2TB RAM GPU:NVIDIA Quadro RTX 8000 48GB RAM

结束语 

这篇博文介绍了一个简单而有效的推荐系统,它可以使用 NetworkX 轻松地用 Python 编写。虽然我们可以采用许多其他方法(如此处所述),但很少有方法能够做到与开始探索 NetworkX 图形分析提供的数据所需的工作量不相上下。然而,高效和有意义的数据探索需要快速的周转,而 NetworkX 传统上一直难以扩展到更大的实际问题规模。

适用于 NetworkX 的 NVIDIA cuGraph 后端可对熟悉且灵活的 NetworkX API 进行加速,还可大规模提升其性能,在几秒钟 (而非数十分钟) 内生成结果,从而让您专注工作并高效工作。现在,用户只需向环境中添加 GPU 和 cuGraph 后端,即可继续使用热门的图形分析库 NetworkX,而无需担心扩展问题。

如需了解有关使用 NetworkX 和 NVIDIA cuGraph 进行加速图形分析的更多信息,请访问 https://rapids.ai/nx-cugraph

 

标签