亚马逊AWS官方博客

使用图数据库 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@<IP Address> -i oregon.pem登录一台EC2示例,然后通过这个示例可以访问Neptune,我们登录这个示例通过下面的命令行就可以导入我们第三步处理好的数据。注意红色部分都要根据你的实际情况做替换。

导入 neptune-vertex-user.csv

curl -X POST -H 'Content-Type: application/json' <LoaderEndpoint> -d '
    {
      "source" : "<neptune-vertex-user S3 path>",
      "format" : "csv",
      "iamRoleArn" :  "<NeptuneLoadFromS3IAMRoleArn>",
      "region" : "us-west-2",
      "failOnError" : "FALSE",	
      "parallelism" : "MEDIUM",
      "updateSingleCardinalityProperties" : "FALSE"
    }'

导入 neptune-vertex-movie.csv

curl -X POST -H 'Content-Type: application/json' <LoaderEndpoint> -d '
    {
      "source" : "<neptune-vertex-movie.csv S3 path>",
      "format" : "csv",
      "iamRoleArn" :  "<NeptuneLoadFromS3IAMRoleArn>",
      "region" : "us-west-2",
      "failOnError" : "FALSE",	
      "parallelism" : "MEDIUM",
      "updateSingleCardinalityProperties" : "FALSE"
    }'

导入 neptune-edge.csv

curl -X POST -H 'Content-Type: application/json' <LoaderEndpoint> -d '
    {
      "source" : "<eptune-edge.csv S3 path>",
      "format" : "csv",
      "iamRoleArn" :  "<NeptuneLoadFromS3IAMRoleArn>",
      "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

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

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

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观影偏好相似用户的其他喜欢的电影

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个人表示喜欢。根据这个热度,我们就可以尝试把靠前的电影推荐给用户。

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,毫无疑问效果更佳。

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解决方案架构师,现主要负责初创企业行业解决方案。成功实施过多个创业项目,具备项目成功出海的架构、开发、运维经验。