使用图数据库 Amazon Neptune 在推荐系统中按照协同过滤的方法做推荐

阅读数:22 2019 年 11 月 27 日 08:00

使用图数据库 Amazon Neptune 在推荐系统中按照协同过滤的方法做推荐

Amazon Neptune 是一项快速、可靠且完全托管的图形数据库服务,可帮助您轻松构建和运行使用高度互连数据集的应用程序。Amazon Neptune 的核心是专门构建的高性能图数据库引擎,它进行了优化以存储数十亿个关系并将图形查询延迟降低到毫秒级。 Amazon Neptune 支持常见的图形模型 Property Graph 和 W3C 的 RDF 及其关联的查询语言. Apache TinkerPop Gremlin 和 SPARQL,从而使您能够轻松构建查询以有效地导航高度互连数据集。Neptune 的使用场景包括推荐引擎、欺诈检测、知识图谱、药物开发和网络安全。

图数据库 Amazon Neptune 自在 2018 年发布以来,凭借其 多种图数据引擎的支持、高可用、多只读副本、跨可用区复制、指定时间点恢复、安全、自动备份等一系列特性,受到了广泛的关注,那么在推荐引擎的设计和开发中怎么用好 Neptune,这一点在官方的相关文档中并没有做深入的说明,本文就这个主题做一个展开讲解,希望对从事相关工作的工程师有所戒借鉴。

图数据库 Neptune 与推荐引擎

通常来说,一个完成的推荐引擎处理推荐请求需要经过多个步骤完成,这些步骤如下:

  • recall(召回) — 决定了哪些内容被找到,作为被推荐的候选内容。

  • score(打分) — 根据被推荐的人特点,对每个找到的内容打分。

  • filter(过滤) — 过滤掉一些不符合业务规则的推荐内容。

  • rank(排序) — 对众多的推荐内容排序,排序的结果决定了返回结果的先后次序。

  • diversify(多样化) — 对于内容相近、重复的内容做处理,使他们不要出现在连续、相近的位置。

在以上各个步骤中,图数据库 Neptune 可以在以其独特的方式在召回打分中表现其独特的作用。我们分别加以说明:

Neptune 在召回中的作用

推荐引擎的召回方法主要有两种,一种是基于推荐内容本身的,方式是对人和内容打标签(标签可以是分类也可以是属性),然后通过标签匹配的方法给人找推荐内容。这种方法的局限性在于,给人推荐的内容逃不过“人的标签”这个怪圈,给人推荐内容缺乏探索性。而协同过滤推荐非常好的弥补了这个缺陷,因为它可以分析人与人、物与物、人与物之间的复杂、交错,而且是连续传递的关系,做探索性的推荐,图数据库 Neptune 非常适合做这种方式的召回。因为,他可以方便、快捷的构建人和物的喜好图谱、知识图谱,来做这种探索性的推荐。

下图是一个人和物的喜好关系图,从这个里面里我们看到 ABC 三个人都 follow 了 SPORT,同时 A 和 B 购买了 PRODUCT,那么这个时候我们就可以尝试给 C 推荐这个 PRODUCT。

下图是一个知识图谱,在图中 Bob 对蒙娜丽莎感兴趣,通过通过分析知识图谱,我们可以发现蒙娜丽莎这幅画是展览在卢浮宫里的,那么我们就可以尝试给 Bob 再推荐卢浮宫里的其他艺术品,如果收获了 Bob 的积极反馈,我们可以藉此更进一步推荐知识图谱中跟其他艺术作品相关的内容。

Neptune 在打分中的作用

探索性的发现,虽然帮我们找到了很多,用户可能感兴趣的内容,但是在现实中这些内容很可能数量众多,我们要对找到的众多内容做一个相似度的排序,才能决定要把哪些内容优先推荐给用户。这个时候我们可以通过图数据库分析大量的关系数据,来得出相似度打分。比如:当我们给用户推荐电影的时候,就可以通过分析用户间的喜好近似度,来判断优先推荐哪个电影。说的更具体一点:假如我们要给 A 推荐电影,通过关系图我们找到了 100 部电影,那这个时候我们可以图查询找到跟 A 的观影历史重合度最高的用户 B,把 B 看过但是 A 没看过的电影优先推荐给 A。这里的“找观影历史重合度高的用户”可以通过 Neptune 非常高效的实现。

在本文中我们将对上述的召回和打分在图数据库 Neptune 中如何实现来做一个详细的说明,在本文中我们将会使用流行的 MovieLens 100K 数据集。

建立一个 Neptune 实例

新建 Neptune 实例的最方便的方式是通过 CloudFormation ,CloudFormation 可以自动化的帮你构建云服务的基础设施栈(CloudFormation Stack),这个 Stack 里面包含云服务里你所需要的各种服务组件,在这里他会帮我们创建一个 VPC 以及里面的 Subnet,在 Subnet 里创建 Neptune,同时创建 EC2 的示例用于连接 Neptune。请点击 新建 Neptune 实例 来创建整个 CloudFormation Stack,整个过程中只有三个地方需要您关注:

  1. 设置 Stack name — 在这里我们设置一个名字 MovieLens,作为我们 CloudFormation Stack 的标识符。

  2. 设置 EC2SSHKeyPairName — 这个地方是在 ssh EC2 instance 时用到的 KeyPair 名字,只需要选在一个你已有的便可。如果你没有话,可以在 EC2 控制台的中找到“Key Pairs”来创建。这一步在“Step 2 Specify stack details里面”。

  3. 授权 CloudFormation 创建相关的 资源 — CloudFormation 在创建整个 Stack 的过程中需要创建代你创建一些资源,这个只要打钩同意便可。这一步在“Step 4 Review 里面”

可以参考下面 3 张图:

当你成功创建了这个 Neptune 的 Stack 后,会看到下面这个图,这个图中 Oupputs 标签展示了这个 Stack 的执行结果。可以看到 CloudFormation 帮我们创建了很多使用 Neptune 的一些必要资源。这些资源包括 VPC、Subnet 等。其中有几项是我们后面在导入数据的时候要用到的,这里已经用绿色方框标出来了。

数据预处理

对于 Neptune 来说数据其实是分成两种的,一种是 vertex 数据,代表顶点;一种是 edge 信息,代表关系。在这里用户和电影都数据都属于 vertex;对于打分数据,我们认为打分超过大于等于 4 分(满分 5 分),代表用户喜欢这个电影,我们把这种信息表示成 like 关系存入 Neptune。我们准备使用 Property Graph 的格式把 MovieLens 的数据导入到 Neptune。在正式导入之前要先对 MovieLens 的数据做一下预处理,以满足 Neptune 的格式要求,MovieLens 的数据主要由 3 部分组成:

  • u.item 这个是电影的数据,包含了电影名字等电影信息。我们把这个转换成 csv 格式的 neptune-vertex-movie.csv 点击下载

  • u.user 这个是用户的数据,包含了用户的职业等用户新消息。我们把这个转换成 csv 格式的 neptune-vertex-user.csv 点击下载

  • u.data 用户对电影的打分数据。我们把这个转换成 csv 格式的 neptune-edge.csv 点击下载

下面分别对上述数据做一个简单说明,先来看 neptune-vertex-user.csv ,文件内容如如下:

~id | ~label | uid:String | age:Int | gender:String | occupation:String | zipcode:String --- | ------ | ---------- | ------- | ------------- | ----------------- | -------------- u1 | user | 1 | 24 | M | technician | 85711 u2 | user | 2 | 53 | F | other | 94043 u3 | user | 3 | 23 | M | writer | 32067

第一行的数据表头中波浪线~ 开头的 ~id 和 ~label 是做为 vertex 文件必须填写的,前者代表了全局 id,后者代表了 vertex 的类型。后面的每个字段都是用冒号: 作为中间的分隔符,这些代表了用户的 property。如果类比关系型数据库的话 label 就相当于 entity,property 就相当于 attribute。也有人把 Property Graph 叫做 LPG(Labeled Property Graph) 就是因为图数据库里的数据格式是通过 label 和 property 来表示的。类似的我们可以看到 neptune-vertex-movie.csv 的格式如下:

~id | ~label | mid:String | title:String --- | ------ | ---------- | ----------------- i1 | item | 1 | Toy Story (1995) i2 | item | 2 | GoldenEye (1995) i3 | item | 3 | Four Rooms (1995)

最后是图数据库中的边 edge 的信息,文件 neptune-edge.csv 的格式如下:

~id | ~from | ~to | ~label | weight:Double --- | ----- | ----- | ------ | ------------- e8 | u253 | i465 | like | 1.0 e12 | u286 | i1014 | like | 1.0 e13 | u200 | i222 | like | 1.0

edge 的格式跟 vertex 的格式要求有所不同,~id ~from ~to ~label 这个四个字段都是必须要填写的字段,分别代表了 edge 的全局 id,edge 的起始 vertex 的全局 id,以及 edge 的 label。在这里 from 和 to 中的 id 分别引用了前面 user 和 movie 的 vertex 的全局 id,label 的取值是 like 代表了 user 和 movie 是喜欢的关系,这个关系是单向的,如果我们把导演的的数据也导入的话,那么导演和电影之间的 edge 的 label 可以起名叫做 direct。

数据导入

完成了以上两部分我们就可以导入数据了,这里我们使用命令行的方式来导入上述数据到刚建立的 Neptune 里。导入的时候要用到我们在第二步”建立一个 Neptune 实例“中用到的几个信息,分别是:

  1. ec2 实例 — 这个实例跟 Neptune 在同一个 VPC 内部,可以通过命令行的方式访问 Neptune。

  2. LoaderEndpoint — 这个是 Neptune 加载数据的 endpoint。

  3. NeptuneLoadFromS3IAMRoleArn — 这个是 CloudFormation 自动创建的 IAM role,用来授权 Neptune 可以从 S3 加载数据。

以上三个信息都可以在第二步 CloudFormation 的 stack 创建成功后的 Outputs 中查看得到,点击回看截图。截图中有一个 SSHAccess,这里告诉我们可以通过 ssh ec2-user@ -i oregon.pem 登录一台 EC2 示例,然后通过这个示例可以访问 Neptune,我们登录这个示例通过下面的命令行就可以导入我们第三步处理好的数据。注意红色部分都要根据你的实际情况做替换。

导入 neptune-vertex-user.csv

curl -X POST -H 'Content-Type: application/json' **** -d '
    {
      "source" : "****",
      "format" : "csv",
      "iamRoleArn" :  "****",
      "region" : "us-west-2",
      "failOnError" : "FALSE",	
      "parallelism" : "MEDIUM",
      "updateSingleCardinalityProperties" : "FALSE"
    }'

导入 neptune-vertex-movie.csv

curl -X POST -H 'Content-Type: application/json' **** -d '
    {
      "source" : "****",
      "format" : "csv",
      "iamRoleArn" :  "****",
      "region" : "us-west-2",
      "failOnError" : "FALSE",	
      "parallelism" : "MEDIUM",
      "updateSingleCardinalityProperties" : "FALSE"
    }'

导入 neptune-edge.csv

curl -X POST -H 'Content-Type: application/json' **** -d '
    {
      "source" : "****",
      "format" : "csv",
      "iamRoleArn" :  "****",
      "region" : "us-west-2",
      "failOnError" : "FALSE",	
      "parallelism" : "MEDIUM",
      "updateSingleCardinalityProperties" : "FALSE"
    }'

完成上述导入后我们可以通过一个可视化工具简单来看一下导入后的效果,从左下角的统计信息可以看到 Neptune 里有 943 个 user 和 1682 个 movie,同时还有 21201 个 like 的单向关系。图中的右半部分是我们展开了其中的几个 user 和 movie 的数据,蓝色的是 user,橙色的是 movie。

协同过滤推荐实现

这里为了方便展示,我们使用 Gremlin Console 这个交互式的查询终端来展示如何用 Neptune 做推荐。安装方法参考:使用 Gremlin Console 连接 Neptune 。通过这个交互式终端能运行的命令都可以方便的使用编程语言嵌入在代码中来使用,比如如果我们使用 Python 就可以使用 pip install gremlinpython 安装与之相关的依赖包来使用我们这里用到的查询语言,你甚至还可以把 Python 代码嵌入到 Lambda 中,实现一个 serverless 方式的图查询来做推荐。

用 Neptune 做协同过滤推荐的召回

我们知道,协同过滤(collaborative filtering)是通过查找跟 A 用户偏好相同人 B,把 B 喜欢的物品推荐给 A。这个过程可以简化成 3 个步骤:

  1. 查找 user A 喜欢什么 item,比如 item A

  2. 查找 item A 还有其他什么人喜欢,比如 user B

  3. 查找 user B 除了 item A 还喜欢什么别的东西,比如 item B,然后把 item B 推荐给 A

以上过程如果通过关系数据库来实现,那么你可能会发现,三个查找步骤都是关联同一个表做 join,这种 SQL 语句写起来复杂,可读性差,维护起来容易引发诸多问题。那么我们这里来看用 Neptune 怎么来实现这三步:

第一步,从简单入手,查找用户 u847 喜欢什么,可以看到返回的结果的全局 id 都是以 i 开头的,代表是 item

Bash

gremlin> g.V('u847').as('user').out('like')
==>v[i109]
==>v[i173]
==>v[i222]
==>v[i239]
==>v[i258]
...

第二步,在第一步的基础上查找:对于 u847 喜欢的电影,还有哪些其他人喜欢

Bash

gremlin> g.V('u847').as('user').out('like')
......1> in('like').where(neq('user'))
==>v[u45]
==>v[u246]
==>v[u5]
==>v[u119]
==>v[u467]
...

第三步,在第二步的基础上,再次查找跟 u847 观影偏好相似用户的其他喜欢的电影

Bash

gremlin> g.V('u847').as('user').out('like')
......1> in('like').where(neq('user')).
......2> out('like').dedup()
==>v[i1]
==>v[i13]
==>v[i50]
==>v[i100]
==>v[i109]
...

以上三步是从易到难,如果想直接获取到协同过滤推荐的结果,直接通过第三步的语句就可以查找到。这里使用的查询语言是 Apache TinkerPop Gremlin,当你熟悉了他的语法后,编写起来就跟写 SQL 类似了,在这里做这种传递关系的查询时,用图数据库查询语言编写的查询语句毫无疑问逻辑上更加清晰,维护起来也相对简单。但是这里的有一点要说明的是,以上三个代码片段的输出结果都是都是截取了前 5 个结果。

用 Neptune 做协同过滤推荐的排序

事实上,当你做上述迭代关系的查询时,得到的结果集都是非常庞大的,如果你听说过六度分隔理论的话不难想象出来。那么,问题就来了,如此庞大的结果,我们选择哪些做为最终推荐结果呢。这个问题在推荐系统中是通过打分、排序来实现的。那么使用 Neptune 怎么对协同过滤的结果做打分排序呢?这里提供两个思路,一个是根据众多结果中流行程度;另外一个思路是查找跟用户 u847 购买重合度高的用户,这些购买重合度高的用户,毫无疑问跟 u847 更加的志趣相投,把这些用户喜欢的电影推荐给 u847 符合它品味的概率会更大:

第一个思路,在上面找到的众多结果中,按照 like 的热度来排序。可以看到排在第一位的电影 i50,有 267 个人喜欢;排在第二位的 i174 的电影哟 179 个人喜欢。由于结果众多,我们省略了中间的部分,可以看到最后 i976 只有 1 个人表示喜欢。根据这个热度,我们就可以尝试把靠前的电影推荐给用户。

Bash

gremlin> g.V('u847').as('user').out('like')
......1> aggregate('self').in('like').where(neq('user')).
......2> out('like').where(without('self')).
......3> groupCount().order(local).by(values, desc)
==>{v[i50]=267, v[i174]=179, v[i181]=175, v[i100]=171, v[i172]=165, v[i56]=158, v[i318]=151, v[i64]=148, v[i98]=147, v[i12]=139, v[i168]=137, v[i127]=129, v[i313]=128, v[i1]=122, v[i96]=120, v[i22]=119, v[i79]=113...v[i973]=1, v[i976]=1}

第二个思路,根据用户的偏好重合度,这里我们查找了跟 u847 购买重合度大于 3 的用户,总共得到了 6 个用户,然后我们可以再尝试把这里的 u125,u151,u180,u416,u472,u457 几个用户喜欢的电影推荐给 u847,毫无疑问效果更佳。

Bash

gremlin> g.V('u847').as('user').out('like')
......1> aggregate('self').in('like').where(neq('user')).dedup().
......2> group().by().by(out('like').where(within('self')).count()).
......3> order(local).by(values,desc).
......4> unfold().filter(select(values).unfold().is(gt(3)))
==>v[u125]=5
==>v[u151]=5
==>v[u180]=4
==>v[u416]=4
==>v[u472]=4
==>v[u457]=4

以上排序的查询使用图数据库查询语言一句话就可以实现,这个在其他的数据库上通常是不能这么简单的来实现的。同时,值得一提的是 Neptune 返回这种查询也是毫秒级的。

总结

综上所述,图数据库 Neptune 是您在 AWS 上快速构建高效推荐系统的不二之选。虽然该服务尚未在 AWS 中国区域尚未发布,但是您依然可以在 Global 区域把 Neptune 作为您的离线计算推荐引擎,把需要做的推荐查询通过离线的方式计算好,然后在国内通过把离线推荐结果部署到 ElastiCache 中来助力您的在业务场景中完成推荐。

作者介绍:

!
### 陈磊

亚马逊 AWS 解决方案架构师,现主要负责初创企业行业

本文转载自 AWS 技术博客。

原文链接:
https://amazonaws-china.com/cn/blogs/china/using-map-database-amazon-neptune-make-recommendations-collaborative-filtering-method/

欲了解 AWS 的更多信息,请访问【AWS 技术专区】

评论

发布