构建实时推荐引擎是Neo4j最常见的用途之一,也是Neo4j强大而易用的用途之一。为了探讨这个问题,我将使用一些数据样例来解释如何运用统计方法到推荐中去。
第一个比较简单——完全使用Cypher语言的社交推荐;之后我们探讨一个涉及可计算相似度度量的相似度推荐;最后是一个聚类推荐。
1 基本的图驱动推荐
下面的数据集包含达拉斯-沃斯堡国际机场提供的食物饮料,该机场是美国主要机场枢纽之一。
我们用黄色标注的场所节点对包括登机门、候机楼的位置进行建模。我们也用食物和饮料两大类对场所进行分类,其中包括墨西哥食物、三明治和酒吧烧烤。
先做一个简单的推荐吧。我们想在机场特定位置里发现一种特定的食物类型,而大括号内的东西表示正在输入到我们假设应用程序的用户输入。
映射到一个Cypher查询:
上图中的category、terminal和gate都会使用用户的请求进行填充。然后我们将得到指定场所到用户所在登机门的绝对距离,并以正序排列。再强调一次,这是一个仅基于机场内用户所在位置的简单Cypher推荐。
2 社交推荐
接下来讨论一下社交推荐。在我们假想的应用程序中,用户可以登陆并类似于Facebook那样「喜欢」某个地方,并对这些地方进行筛选:
考虑基于我们探讨过的第一个模型上的这个数据模型,现在我们需要在如下分类中找到某个饮食场所,而这个地方最接近用户的朋友喜欢的候机楼的登机门。
MATCH子句与我们在第一个Cypher查询中用到的MATCH子句类似,但不同的是我们现在是在匹配朋友和喜欢:
前三行是相同的,对于已经登录的用户,我们希望能通过FRIENDS_WITH关系找到他的朋友以便找到朋友们喜欢的饮食场所。添加少许行数到Cypher之后,我们的推荐引擎现在已考虑到社交方面了。
再次强调,我们仅显示用户明确要求是与用户同一候机楼的类型。当然,我们想通过已登录并正在发出请求的用户对这一结果进行筛选,并根据类型与位置返回这些场所的名字。我们也考虑了喜欢这个场所的朋友人数和这个场所到登机门的几何距离。返回都写在RETURN子句中。
3 相似度推荐
现在我们再看一下一个相似度推荐引擎:
类似于我们之前用户可以「喜欢」某些地方的数据模型,不过这次他们需要给这些地方打0至10分。对于Neo4j来说给这些关系添加一个属性十分简单。
这样我们可以寻找其他类似的用户,比如Greta和Alice的例子。我们已经知道他们都喜欢的地方和他们给每一个地方的分数。进而我们可以使用这些分数判定他们之间的相似度。
现在我们有两组向量:
计算这两组向量的欧几里得几何距离:
代入所有数之后,我们就能得到如下真正能说明两个用户相似度的度量:
在Cypher中你可以计算两个特定用户的相似度,特别是他们共同喜欢的地方较少时。再次说明,我们在匹配Greta和Alice两个用户并试图找到他们共同喜欢的地方:
上述Cypher语句中将查询都有:LIKES关系连接某一个地方的两个用户,之后用各元素平方和的开根号计算他们的欧几里得几何距离。
虽然在两个特殊用户的例子里这么做可能有用,但在实时推荐中,当你想通过在数据库中实时匹配其他用户来实时推测出与另一用户相似的用户时,其实也没必要这么做。说实话,这也没那么有效。
为了解决这个问题,我们先对这些计算做预处理,并在实际关系中存储这一结果:
虽然在大数据集中我们可以成批做这种计算,但在小的样本数据集中,我们应该与所有用户匹配他们共同喜欢的地方的笛卡尔积。我们在Cypher语句中使用WHERE id(u1) < id(u2)以保证查找到只有左右关系不相同的结果。
之后我们根据用户本身及其两者间的欧几里得几何距离创建一对:DISTANCE关系,并设置一个欧几里得属性euclidean。理论上,我们也可以存储用户间关系的其他相似度量来描述不同的相似尺度,因为有一些相似尺度在确定的环境中比其他的有用。
Neo4j这种能在关系上建模的能力让事情处理起来相当简单。但事实上,你不会去存储每一个可能会存在的关系,因为你会只想返回与之相邻的顶层少部分人而已。
所以你可以通过设置一些阈值保留少数关系从而抛弃全连接图。这将允许你进行如下实时查询图数据库,因为我们已预处理并存储在关系上,使用Cypher查询可以快速获取结果:
在这个查询里,我们匹配了地方和类别:
前三行与之前相同,但对于已经登录的用户而言,我们将获取与之有:DISTANCE关系用户而已。我们之前做的努力就在这里体现出来了——实际上你只需要存储与之相似的少数用户的:DISTANCE关系便无需在MATCH子句中操作大量的用户。相反,我们只获取与之有:DISTANCE关系用户,且这些用户喜欢这个地方。
由此我们仅需几行便可以表达复杂的模式。我们同时也获取:LIKES关系并保存在变量里,因为我们之后应用到比值时会使用这些权重。
更重要的是我们使用距离正序排列这些用户,因为我们可以使用这种距离度量让最短距离表示最相似。
用欧几里得距离排序完这些用户之后,获得距离最小的三个用户的评分,并使用三个评分作为推荐场所的平均分数。换句话说,我们假定一个在线用户,根据共同喜欢的地方发现跟他们相似的潜在用户,然后计算这些用户给这些地方打的平均分数并作为一个结果集。
我们本质上是通过计算集合中元素的总和并除以其个数来得到平均数,之后便可使用这些平均数正序排列。其次我们再使用到登机门的距离值排列。我假定之后你会通过排列登机门距离返回name,category,gate和terminal等结果。
4 聚类推荐
最后的例子是一个聚类推荐,这种推荐可以看成离线计算的工作流,也就是Cypher中比较变通的做法。由于GraphConnect Europe公布了一些新特性,所以这种变通可能会有些过时。
但有时你也不得不实现一些Cypher 2.3版本没有公开的算法。
这时你可以使用一些统计软件,如Apache Spark、R或Python,将Neo4j的数据导出。下面是使用R代码导出数据的例子,运行程序,然后在合适时机,将程序结果作为属性、节点、关系或标签写回Neo4j。
将程序结果写回图之后,你将会在实时系统中用到类似如下的查询:
下面是一些R语言实现的代码例程,你也可以用其他习惯的软件来写,比如Python或Spark。你要做的就是登陆和连接到这个图。
在下面的例子中,我根据相似度将用户进行聚类操作。每一个用户都表示一个单独对象,并统计他们对每一种分类的打分:
可能对酒吧分类的打分相似的用户在其他方面也相似。这里将喜欢同一地方分类的用户名、分类名、「喜欢」关系的平均权值列出一张类似如下图的表格:
因为我们将每个用户都单独列出,所以我们不得不去计算用户的评分数据,这些数据每一个特征都是他们给每一个分类的平均评分。我们之后会使用这个来度量他们之间的相似程度,并用一个聚类算法决定将用户分配到哪个聚类中。
在R里,这实现起来很简单:
在这个演示里,我们使用了比较容易进行聚类分配的K-均值法。总的来说,就是运行了一个聚类算法并对每个用户进行聚类分配。
Bob和David都在二号类里——现在可以实时看到哪些用户被分配到同一个聚类里。
将结果写入一个CSV文件中,用来转入图中:
我们只需要用户和聚类分配结果,所以CSV文件只需要两个字段就可以了。Cypher里有一个内建的LOAD CSV语法,能让我们从某些文件路径、URL或别名中调用CSV文件。之后我们会通过已存在图里的数据匹配用户,提取CSV文件中用户字段合并用户聚类到图里面去。
这里,我们在图中创建了一个新的节点,命名为Cluster ID。之后在用户和聚类之间建立关系,这些关系可以让我们在同一聚类中获取实际推荐用户时简化查询。
现在我们有标记簇,其中同一簇的用户都会有到该簇的关系连接。我们的数据模型看起来像下面那样,不过这只是我们已建立模型中的局部:
现在看下这个查询:
在这个Cypher查询中,我们使用同簇用户而不是相似用户,这时我们也该删除距离关系了:
在这个查询中,我们抓取已经登录的用户,在用户簇关系的基础上找到他们的簇,并在同一个簇中找到他们的邻居。
我们给变量cl赋值,在同一个簇中,我们得到那些拥有用户簇关系的其他用户,将它们放在别名为neighbor的变量中,然后得到了那个邻居喜欢的地方。再次,我们把“likes”放在变量r中,因为我们想要从关系中提取出权重,用来排序我们的结果。
我们在查询中改变的是,没有使用相似距离,取而代之的是我们抓取在同一个簇中的用户,声明categories、terminal以及抓取登陆的用户。我们从他们的邻居喜欢的地方收集所有“likes”的关系权重,得到类别,距离的绝对值,以降序排序,并返回这些结果。
在这些例子中,我们已经能够使用比较复杂处理并用图存储结果,之后可以实时使用这些算法结果——即聚类算法结果和聚类分配。
我们更倾向于可以在合适周期,比如每天或每个小时更新这些聚类分配的工作方式。当然,你也可以靠直觉决定合适的更新周期。