关于推荐:实操结合图数据库图算法机器学习GNN-实现一个推荐系统

8次阅读

共计 20876 个字符,预计需要花费 53 分钟才能阅读完成。

本文是一个基于 NebulaGraph 上图算法、图数据库、机器学习、GNN 的举荐零碎办法综述,大部分介绍的办法提供了 Playground 供大家学习。

基本概念

举荐零碎诞生的初衷是解决互联网时代才面临的信息量过载问题,从最后的 Amazon 图书举荐、商品举荐,到电影、音乐、视频、新闻举荐,现在大多数网站、App 中都有至多一个基于举荐系统生成的供用户抉择的物品列表界面。而这些物品的举荐根本都是 基于用户爱好、物品的特色、用户与物品交互历史和其余相干上下文 去做的。

一个举荐零碎会蕴含以下几个局部:

  • 数据、特色的解决
  • 从特色登程,生成举荐列表
  • 过滤、排序举荐列表

这其中,过滤的外围办法次要有两种:基于内容的过滤 Content-Based Filtering、与协同过滤 Collaborative Filtering,相干论问介绍可参考延长浏览 1、2。

基于内容的过滤

内容过滤办法的实质是给用户的偏好做画像,同时对所有待举荐的物品计算特色,做用户的画像与待举荐物品特色之间的间隔运算,过滤失去相近的物品。。

内容过滤的办法的益处有:

  • 清晰的可解释性,无论是对用户的画像剖析,还是对物品的运算自身人造带来了排序、过滤的可解释性;
  • 用户数据输出的独立性,对指定的待举荐用户来说,只须要独自剖析他们的画像和历史评分就足够了;
  • 躲避新物品冷启动问题,对于新的增加的物品,即便没有任何历史的用户评估,也能够做出举荐;

同时,基于内容过滤的举荐也有以下弊病:

  • 特征提取难度,比方:照片、视频等非纯文本数据,它们的特征提取依赖领域专家常识。举个例子,电影举荐零碎须要抽出导演、电影分类等畛域常识作为特色;
  • 难以突破舒服圈,挖掘用户的潜在新趣味点;
  • 新用户冷启动数据缺失问题,新用户个人信息少难以造成用户画像,进而短少做物品画像、特种间隔运算的输出;

基于协同过滤

协同过滤的实质是联合用户与零碎之间的交互行为去举荐物品。

协同过滤的办法又分为 基于记忆 memory-based 的协同过滤与 基于模型 model-based 的协同过滤。

基于记忆的协同过滤次要有物品与物品之间的协同过滤 ItemCF 和用户与用户之间的协同过滤 UserCF。ItemCF 简略来说是,举荐和用户之前抉择类似的物品,即:依据行为找物品之间的相似性;UserCF 则举荐与用户有共同爱好的人所喜爱的物品,即:依据行为找用户之间的相似性。

基于模型的协同过滤次要依据用户爱好的历史信息、利用统计与机器学习办法训练模型,对新用户的偏好进行推理。

协同过滤的办法的益处有:

  • 无需对非结构化物品进行特征分析,因为协同过滤关注的是用户和物品之间的协同交互,这绕过了对物品畛域常识解决的需要;
  • 对用户的个性化定制水平更强、更细,基于行为的剖析使得对用户偏好的划分实质上是间断的(相比来说,对用户做画像的办法则是离散的),这样的举荐后果会更加“千人千面”。同时,也会蕴含内容过滤、无限的画像角度之下的“惊喜”举荐。

但,它的毛病有以下方面:

  • 有新用户和新物件上的冷启动问题,因为它们身上都短少历史爱好行为的信息;

咱们总结一下,两种过滤形式各有利弊,也存在互补的中央。比方,新物件的冷启动上,基于内容的过滤有劣势;对于个性化、举荐惊喜度方面,协同过滤有劣势。所以,在实操中,举荐零碎大多演变都比下面的归类简单得多,而且经常随同着多种办法的交融。

基于图的共性举荐

图技术、图数据库技术在举荐零碎中的利用是多方面的,在本章节中咱们会从图数据库的出发点上给出多种利用例子。

建设图谱

在开始之前,我简略地介绍下本文应用的图数据集。

为了给出更靠近理论状况的例子,我从两个公开的数据集 OMDB 和 MovieLens 中别离抽取了所需信息,组成了一个既蕴含电影的卡司(导演、演员)和类型,又蕴含用户对电影评分记录的常识图谱。

Schema 如下:

  • 顶点:

    • user(user_id)
    • movie(name)
    • person(name, birthdate)
    • genre(name)
  • 边:

    • watched(rate(double))
    • with_genre
    • directed_by
    • acted_by

这个数据的筹备、ETL 过程会在另外的文章里具体介绍,在进入下一章节之前,咱们能够用 Nebula-Up 一键搭起一个测试的 NebulaGraph 单机集群,再参考数据集的 GitHub 仓库,一键导入所需数据,数据集参考延长浏览 4。

具体操作步骤:

  1. 用 Nebula-Up 装置 NebulaGraph;
  2. 克隆 movie-recommendation-dataset;
  3. 导入数据集 NebulaGraph;
curl -fsSL nebula-up.siwei.io/install.sh | bash

git clone https://github.com/wey-gu/movie-recommendation-dataset.git && cd movie-recommendation-dataset

docker run --rm -ti \
    --network=nebula-net \
    -v ${PWD}:/root/ \
    -v ${PWD}/dbt_project/to_nebulagraph/:/data \
    vesoft/nebula-importer:v3.2.0 \
    --config /root/nebula-importer.yaml

基于内容的过滤

CBF,内容过滤的思维是利用畛域常识、历史记录、元数据别离对用户和物件做画像、打标签,最终依据用户的标签与待举荐物件之间的间隔评分进行排序给出相干举荐。

对用户的画像不波及其余用户的信息,然而输出的特色可能来源于元数据(生日、国籍、性别、年龄)、历史记录(评论、打分、购买、浏览)等等,在这些根底之上对用户进行标签标注、分类、聚类。

对物件的画像输出的特色可能是基于语言解决(NLP、TF-IDF、LFM)、专家标注、多媒体解决(视觉到文字再 NLP、音频格调解决、音频到文字再 NLP)等。

有了用户画像与物件的画像特色、对用户波及的画像进行相干画像物件中新对象的近似度计算,再评分加权,就能够取得最终的举荐排序了。其中的近似度计算常见的有 KNN、余弦类似度、Jaccard 等算法。

CBF 的办法中没有限定具体实现形式。如前边介绍,可能是基于机器学习、Elasticsearch、图谱等不同办法。为切合本章的主题,这里我给出一个基于图数据库、图谱上的 CBF 的例子,做一个电影举荐零碎,能让读者了解这个办法的思维。同时,也能相熟图数据库、常识图谱的办法。

实操局部的用户特色间接利用历史电影评估记录,而举荐物件「电影」的画像则来自于畛域中的常识。这些常识有:电影格调、电影的卡司、导演。近似度算法则采纳图谱中基于关系的 Jaccard 类似度算法。

Jaccard Index

Jaccard Index 是一个形容两个汇合间隔的定义公式,非常简单、合乎直觉地取两者的交加与并集测度的比例,它的定义记为:

这里,咱们把交加了解为 A 与 B 独特连贯的点(有独特的导演、电影类型、演员),并集了解为这几种关系下与 A 或者 B 直连的所有点,而测度就间接用数量示意。

CBF 办法在 NebulaGraph 中的实现

CBF 办法分如下四步:

  1. 找出举荐用户评分过的电影;
  2. 从用户评分过的电影,经由导演卡司、电影类型找到新的待举荐电影;
  3. 对看过的电影与新的电影,藉由导演、卡司、电影类型的关系,在图上做 Jaccard 相似性运算,得出每一对看过的电影和待举荐新电影之间的 Jaccard 系数;
  4. 把用户对看过电影的评分作为加权系数,针对其到每一个新电影之间的 Jaccard 系数加权评分,取得排序后的举荐电影列表;
// 用户 u_124 看过的电影
MATCH (u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"
WITH collect(id(m)) AS watched_movies_id

// 依据电影的标注关系找到备选举荐电影,刨除看过的,把评分、交加关联链路的数量传下去
MATCH (u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)
WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id
WITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50

// 计算 Jaccard index-------------------------------------------------

// 针对每一对 m 和 recomm:// 开始计算看过的电影,汇合 a 的局部
MATCH (m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)
WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a

// 计算举荐电影,汇合 b 的局部
MATCH (recomm:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)
WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b

// 失去并集数量
WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B

// 失去每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_B
WITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index

// 计算 Jaccard index-------------------------------------------------


// 失去每一个被举荐的电影 recomm_id,经由不同看过电影举荐链路的类似度 = 评分 * jaccard_index
WITH recomm_id, m_id, (rate * jaccard_index) AS score

// 对每一个 recomm_id 依照 m_id 加权求得类似度的和,为总的举荐水平评分,降序排列
WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0
RETURN recomm_id, sim_score LIMIT 50

上边查问的执行后果截取进去是:

recomm_id sim_score
1891 0.2705882352941177
1892 0.22278481012658227
1894 0.15555555555555556
808 0.144
1895 0.13999999999999999
85 0.12631578947368421
348 0.12413793103448277
18746 0.11666666666666668
628 0.11636363636363636
3005 0.10566037735849057

可视化剖析

咱们把下面过程中的局部步骤查问批改一下为 p=xxx 的形式,并渲染进去,会更加不便了解。

例子 1,用户 u_124 看过的、评分过的电影:

// 用户 u_124 看过的电影
MATCH p=(u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"
RETURN p

例子 2,找到用户 u_124 看过的那些电影在雷同的演员、导演、电影类型的关系图谱上关联的所有其余电影:

// 用户 u_124 看过的电影
MATCH (u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"
WITH collect(id(m)) AS watched_movies_id
  
// 依据电影的标注关系找到备选举荐电影,刨除看过的,把评分、交加关联链路的数量传下去
MATCH p=(u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)
WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id
RETURN p, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 500

能够看到用户 u_124 看过电影经由演员、类型扩散出好多新的电影

在失去待举荐电影以及举荐门路后,通过 Jaccard 系数与用户门路第一条边的评分综合评定之后,失去了最终的后果。

这里,咱们把后果再可视化一下:获得它们和用户之间的门路并渲染进去。

// 用户 u_124 看过的电影
MATCH (u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"
WITH collect(id(m)) AS watched_movies_id

// 依据电影的标注关系找到备选举荐电影,刨除看过的,把评分、交加关联链路的数量传下去
MATCH (u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)
WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id
WITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50

// 计算 Jaccard index-------------------------------------------------

// 针对每一对 m 和 recomm:// 开始计算看过的电影,汇合 a 的局部
MATCH (m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)
WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a

// 计算举荐电影,汇合 b 的局部
MATCH (recomm:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)
WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b

// 失去并集数量
WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B

// 失去每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_B
WITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index

// 计算 Jaccard index-------------------------------------------------


// 失去每一个被举荐的电影 recomm_id,经由不同看过电影举荐链路的类似度 = 评分 * jaccard_index
WITH recomm_id, m_id, (rate * jaccard_index) AS score

// 对每一个 recomm_id 依照 m_id 加权求得类似度的和,为总的举荐水平评分,降序排列
WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0
WITH recomm_id, sim_score LIMIT 10
WITH COLLECT(recomm_id) AS recomm_ids
MATCH p = (u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)
WHERE id(u) == "u_124" AND id(recomm) in recomm_ids
RETURN p

哇,咱们能够很清晰地看到举荐的理由门路:喜爱星战的用户通过多条独特的类型、演员、导演的边疏导出未观看的几部星战电影。这其实就是 CBF 的劣势之一:人造具备较好的可解释性

基于记忆的协同过滤

前边咱们提过了,协同过滤次要能够分为两种:

  • User-User CF 基于多个用户对物件的历史行为,断定用户之间的相似性,再依据类似用户的抉择举荐新的物件;
  • Item-Item CF 判断物件之间的相似性,给用户举荐他喜爱的物品类似的物品。

这里,ItemCF 看起来和前边的 CBF 有些相似,外围区别在于 CBF 找到类似物件的形式是基于物件的“内容”自身,是畛域常识的画像,而 ItemCF 的协同则是思考用户对物件的历史行为

ItemCF

这个办法分如下四步:

  1. 找出举荐用户评分过的电影;
  2. 经由用户的高评分电影,找到其余给出高评分用户所看过的新的高评分电影;
  3. 通过用户的评分对看过的电影与新的电影在图上做 Jaccard 相似性运算,得出每一对看过的电影和待举荐新电影之间的 Jaccard 系数;
  4. 把用户对看过电影的评分作为加权系数,针对其到每一个新电影之间的 Jaccard 系数加权评分,取得排序后的举荐电影列表。
// 用户 u_124 看过的并给出高评分的电影
MATCH (u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3
WITH collect(id(m)) AS watched_movies_id

// 依据同样也看过这些电影,并给出高评分的用户,得出待举荐的电影
MATCH (u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)
WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id AND e0.rate >3 AND e1.rate > 3
WITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50

// 计算 Jaccard index-------------------------------------------------

// 针对每一对 m 和 recomm:// 开始计算看过的电影,汇合 a 的局部
MATCH (m:`movie`)<-[e0:watched]-(intersection:`user`)
WHERE e0.rate >3
WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a

// 计算举荐电影,汇合 b 的局部
MATCH (recomm:`movie`)<-[e1:watched]-(intersection:`user`)
WHERE e1.rate >3
WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b

// 失去并集数量
WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B

// 失去每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_B
WITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index

// 计算 Jaccard index-------------------------------------------------


// 失去每一个被举荐的电影 recomm_id,经由不同看过电影举荐链路的类似度 = 评分 * jaccard_index
WITH recomm_id, m_id, (rate * jaccard_index) AS score

// 对每一个 recomm_id 依照 m_id 加权求得类似度的和,为总的举荐水平评分,降序排列,只取正值
WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0
RETURN recomm_id, sim_score LIMIT 50

后果:

recomm_id sim_score
832 0.8428369424692955
114707 0.7913842214590154
64957 0.6924673321504288
120880 0.5775219768736295
807 0.497532028328161
473 0.4748322300870322
52797 0.2311965559170528
12768 0.19642857142857142
167058 0.19642857142857142

可视化剖析 ItemCF

同样,咱们把整个过程中的局部步骤查问批改一下为 p=xxx 的形式,并渲染进去,看看有什么有意思的的洞察。

步骤 1 的例子,找出举荐用户评分过的电影:

// 用户 u_124 看过的并给出高评分的电影
MATCH p=(u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3
RETURN p

它们是:

步骤 2 的例子,经由用户的高评分电影,找到其余给出高评分用户所看过的新的高评分电影,批改后果为门路:

// 用户 u_124 看过的并给出高评分的电影
MATCH (u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3
WITH collect(id(m)) AS watched_movies_id

// 依据同样也看过这些电影,并给出高评分的用户,得出待举荐的电影
MATCH p=(u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)
WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id AND e0.rate >3 AND e1.rate > 3
WITH p, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 200

能够看到待举荐的电影在门路的左边末端,两头连贯着的都是其余用户的举荐记录,它的模式和 CBF 真的很像,只不过关联的关系不是具体的内容,而是行为。

步骤 3 的例子,在得出每一对看过的电影和待举荐新电影之间的 Jaccard 系数之后,把用户对看过电影的评分作为加权系数,针对其到每一个新电影之间的 Jaccard 系数加权评分,取得排序后的举荐电影列表。这里革新下最终的查问为门路,并渲染前 500 条门路:

// 用户 u_124 看过的并给出高评分的电影
MATCH (u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3
WITH collect(id(m)) AS watched_movies_id

// 依据同样也看过这些电影,并给出高评分的用户,得出待举荐的电影
MATCH (u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)
WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id AND e0.rate >3 AND e1.rate > 3
WITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50

// 计算 Jaccard index-------------------------------------------------

// 针对每一对 m 和 recomm:// 开始计算看过的电影,汇合 a 的局部
MATCH (m:`movie`)<-[e0:watched]-(intersection:`user`)
WHERE e0.rate >3
WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a

// 计算举荐电影,汇合 b 的局部
MATCH (recomm:`movie`)<-[e1:watched]-(intersection:`user`)
WHERE e1.rate >3
WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b

// 失去并集数量
WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B

// 失去每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_B
WITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index

// 计算 Jaccard index-------------------------------------------------


// 失去每一个被举荐的电影 recomm_id,经由不同看过电影举荐链路的类似度 = 评分 * jaccard_index
WITH recomm_id, m_id, (rate * jaccard_index) AS score

// 对每一个 recomm_id 依照 m_id 加权求得类似度的和,为总的举荐水平评分,降序排列,只取正值
WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0
WITH recomm_id LIMIT 10
WITH COLLECT(recomm_id) AS recomm_ids
MATCH p = (u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)
WHERE id(u) == "u_124" AND id(recomm) in recomm_ids
RETURN p limit 500

能够看出最举荐的两部电影简直所有人看过,且是给过中高评分的用户独特看过的电影:

对于”高评分“

这里有个优化点,“高评分”其实是高于 3 的评分。这样的设定会有失公允,更正当的形式是针对每一个用户,获得这个用户所有评分的平均值,再获得与平均值相差的比例或者绝对值断定高下。此外,在通过 Jaccard 相似性判断每一部看过的电影和对应举荐电影的相似性时,并没有思考这层关联关系:

(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)

e0 与 e1 的评分数值的作用因素只过滤了低评分的关系,能够做进一步优化。

Pearson Correlation Coefficient

皮尔逊积矩相关系数 Pearson Correlation Coefficient,就是思考了关系中的数值进行相似性运算的办法。

Pearson Correlation Coefficient,缩写 PCC。其定义为:

相比 Jaccard Index,它把对象之间关系中的数值与本身和所有对象的数值的平均值的差进行累加运算,在思考了数值比重的同时思考了数值基于对象本身的绝对差别。

UserCF

上面,咱们就利用 Pearson Correlation Coefficient 来举例 UserCF 办法。

基于用户的协同过滤办法分如下四步:

  1. 找出和举荐用户同样给出评分过的电影的用户;
  2. 运算 Pearson Correlation Coefficient 失去和举荐用户趣味靠近的用户;
  3. 通过趣味靠近用户失去高评分未观看电影;
  4. 依据观看用户的 Pearson Correlation Coefficient 加权,排序得举荐电影列表;
// 找出和用户 u_2 看过雷同电影的用户, 得电影评分
MATCH (u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)
WHERE id(u) == "u_2" AND id(u_sim) != "u_2"
WITH u, u_sim, collect(id(m)) AS watched_movies_id, COLLECT({e0: e0, e1: e1}) AS e

// 计算 u_2 和这些用户的 pearson_cc
MATCH (u:`user`)-[e0:watched]->(m:`movie`)
WITH u_sim, watched_movies_id, avg(e0.rate) AS u_mean, e

MATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)
WITH u_sim, watched_movies_id, u_mean, avg(e1.rate) AS u_sim_mean, e AS e_

UNWIND e_ AS e
WITH sum((e.e0.rate - u_mean) * (e.e1.rate - u_sim_mean) ) AS numerator,
     sqrt(sum(exp2(e.e0.rate - u_mean)) * sum(exp2(e.e1.rate - u_sim_mean))) AS denominator,
     u_sim, watched_movies_id WHERE denominator != 0

// 取 pearson_cc 最大的 50 个类似用户
WITH u_sim, watched_movies_id, numerator/denominator AS pearson_cc ORDER BY pearson_cc DESC LIMIT 50

// 取类似用户给出高评分的新电影,依据类似用户个数对用户类似水平 pearson_cc 加权,取得举荐列表
MATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)
WHERE NOT id(recomm) IN watched_movies_id AND e1.rate > 3
RETURN recomm, sum(pearson_cc) AS sim_score ORDER BY sim_score DESC LIMIT 50

后果:

recomm sim_score
(“120880” :movie{name: “I The Movie”}) 33.018868012270396
(“167058” :movie{name: “We”}) 22.38531462958867
(“12768” :movie{name: “We”}) 22.38531462958867
(“55207” :movie{name: “Silence”}) 22.342886447570585
(“170339” :movie{name: “Silence”}) 22.342886447570585
(“114707” :movie{name: “Raid”}) 21.384280909249796
(“10” :movie{name: “Star Wars”}) 19.51546960750133
(“11” :movie{name: “Star Wars”}) 19.515469607501327
(“64957” :movie{name: “Mat”}) 18.142639694676603
(“187689” :movie{name: “Sin”}) 18.078111338733557

可视化剖析 UserCF

再看看 UserCF 的可视化后果吧:

步骤 1 的例子,找出和举荐用户同样给出评分过的电影的用户:

// 找出和用户 u_2 看过雷同电影的用户
MATCH p=(u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)
WHERE id(u) == "u_2" AND id(u_sim) != "u_2"
RETURN p

步骤 2 的例子,运算 Pearson Correlation Coefficient 失去和举荐用户趣味靠近的用户,输入这些靠近的用户:

// 找出和用户 u_2 看过雷同电影的用户, 得电影评分
MATCH (u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)
WHERE id(u) == "u_2" AND id(u_sim) != "u_2"
WITH u, u_sim, collect(id(m)) AS watched_movies_id, COLLECT({e0: e0, e1: e1}) AS e

// 计算 u_2 和这些用户的 pearson_cc
MATCH (u:`user`)-[e0:watched]->(m:`movie`)
WITH u_sim, watched_movies_id, avg(e0.rate) AS u_mean, e

MATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)
WITH u_sim, watched_movies_id, u_mean, avg(e1.rate) AS u_sim_mean, e AS e_

UNWIND e_ AS e
WITH sum((e.e0.rate - u_mean) * (e.e1.rate - u_sim_mean) ) AS numerator,
 sqrt(sum(exp2(e.e0.rate - u_mean)) * sum(exp2(e.e1.rate - u_sim_mean))) AS denominator,
 u_sim, watched_movies_id WHERE denominator != 0

// 取 pearson_cc 最大的 50 个类似用户
WITH u_sim, watched_movies_id, numerator/denominator AS pearson_cc ORDER BY pearson_cc DESC LIMIT 50

RETURN u_sim

咱们给它们标记一下色彩:

步骤 3 的例子,通过趣味靠近用户失去高评分未观看电影,依据观看用户的 Pearson Correlation Coefficient 加权,排序得举荐电影列表。咱们把后果输入为这些类似用户的高评分电影门路:

// 找出和用户 u_2 看过雷同电影的用户, 得电影评分
MATCH (u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)
WHERE id(u) == "u_2" AND id(u_sim) != "u_2"
WITH u, u_sim, collect(id(m)) AS watched_movies_id, COLLECT({e0: e0, e1: e1}) AS e

// 计算 u_2 和这些用户的 pearson_cc
MATCH (u:`user`)-[e0:watched]->(m:`movie`)
WITH u_sim, watched_movies_id, avg(e0.rate) AS u_mean, e

MATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)
WITH u_sim, watched_movies_id, u_mean, avg(e1.rate) AS u_sim_mean, e AS e_

UNWIND e_ AS e
WITH sum((e.e0.rate - u_mean) * (e.e1.rate - u_sim_mean) ) AS numerator,
 sqrt(sum(exp2(e.e0.rate - u_mean)) * sum(exp2(e.e1.rate - u_sim_mean))) AS denominator,
 u_sim, watched_movies_id WHERE denominator != 0

// 取 pearson_cc 最大的 50 个类似用户
WITH u_sim, watched_movies_id, numerator/denominator AS pearson_cc ORDER BY pearson_cc DESC LIMIT 50

// 取类似用户给出高评分的新电影,依据类似用户个数对用户类似水平 pearson_cc 加权,取得举荐列表
MATCH p=(u_sim:`user`)-[e1:watched]->(recomm:`movie`)
WHERE NOT id(recomm) IN watched_movies_id AND e1.rate > 3
WITH p, recomm, sum(pearson_cc) AS sim_score ORDER BY sim_score DESC LIMIT 50
RETURN p

失去的后果,咱们增量渲染到画布上能够失去:这些 UserCF 举荐而得的电影在门路末端:

在可视化中,类似用户的高评分未观看电影被举荐的思维是不是高深莫测了呢?

混合的办法

在实在的利用场景里,达到最好成果的办法通常是联合不同的协同办法,这样既能够让不同角度的有用信息失去充分利用,又能补救繁多办法在不同数据量、不同阶段的弱点。

具体的联合办法在工程上千差万别,这里就不做开展。不过,抛砖引玉咱们来看一种基于模型的混合形式。

基于模型的办法

下面讲过基于内容的(电影的畛域常识、关系)、协同的(用户与用户、用户与电影之间的交互关系)的算法来间接进行相干举荐。但实际上,它们也能够作为机器学习的输出特色,用统计学的办法失去一个模型,用来预测用户可能喜爱的物件(电影),这就是基于模型的办法。

基于模型的办法能够很天然地把以上几种过滤办法作为特色,实质上这也是混合过滤办法的一种实现。

基于模型、机器学习的举荐零碎办法有很多,这里着重同图、图数据库相干的办法,解说其中基于图神经网络 GNN 办法。

GNN 的办法能够将图谱中的内容信息(导演、演员、类型)和协同信息(用户 - 用户、电影 - 电影、用户 - 电影之间的互相关系)以常识的形式嵌入,并且办法中的消息传递形式保有了图中的局部性(locality),这使得它可能成为一个十分新鲜、无效的举荐零碎模型办法。

GNN + 图数据库的举荐零碎

为什么须要图数据库

GNN 的办法中,图数据库只是一个可选项,而我给的 GNN 办法的示例中,图数据库的关键作用是它带来了实时性的可能

一个实时性举荐零碎要求在秒级响应下利用 GNN 训练模型从近实时的输出数据中进行推理,这给咱们提出的要求是:

  • 输出数据能够实时、近实时获取;
  • 推理运算能够实时实现;

而利用演绎型 Inductive model 的模型从图数据库中实时获取新的数据子图作为推理输出是一个满足这样要求可行的设计形式。

下边是简略的流程图:

  • 右边是模型训练,在图谱的二分图(user 和 item)之上建模

    • 用户和物件(movie)之间的关系除了交互关系之外,还有事后解决的关系,这些关系被查问取得后再写回图数据库中供后续生产应用,关系有:

      • 用户间“类似”关系
      • 物件间不同(独特演员、类型、导演)关系
    • 利用 Nebula-DGL 将图中须要的点、边序列化为图深度学习框架 DGL 能够生产的对象
    • 在 DGL 中宰割训练、测试、验证集,利用 PinSAGE 模型训练
    • 导出模型给举荐零碎应用
  • 左边是导出的模型作为推理接口的举荐零碎

    • 基于图库的实时图谱上始终会有实时的数据更新,节点增减
    • 当给定的用户举荐申请过去的时候,图库中以该用户为终点的子图会被获取(1.)、作为输出发送给推理接口(2.)
    • 推理接口把子图输出给之前训练的模型,取得该用户在子图中关联的新物件中的评分排序(3.)作为举荐后果

因为篇幅关系,这里不做端到端的实例代码展现,后续有机会我会出个 demo。

举荐零碎可解释性

在完结本章之前,最初举一个图数据库在举荐零碎中的典型利用:举荐理由。

下图是美团、公众点评中的一个常见的搜寻、举荐后果。当初举荐零碎的复杂度十分高,一方面由实现形式的个性决定,另一方面最终举荐由多个协同零碎组合生成最终排名,这使得零碎很难对举荐后果进行解释。

得益于被举荐用户和物件、以及他们的各种各样画像最终造成的常识图谱,咱们只须要在图谱中对举荐后果进行“门路查找”就能够取得很有意义的解释,像是如下截图的“在北京喜爱北京菜的山东老乡都说这家店很赞”就是这样取得的解释。

图片起源:美团案例分享

可解释性的例子

回到咱们电影举荐的图谱上,前边的算法中咱们取得过用户 u_124 的举荐电影 1891(星球大战),那么咱们能够通过这一个查问取得它的举荐解释:

FIND NOLOOP PATH FROM "u_124" TO "1891" over * BIDIRECT UPTO 4 STEPS yield path as `p` | LIMIT 20

咱们能够很快取得 20 条门路:

(root@nebula) [moviegraph]> FIND NOLOOP PATH FROM "u_124" TO "1891" over * BIDIRECT UPTO 4 STEPS yield path as `p` | LIMIT 20
+-----------------------------------------------------------------------------------------------------+
| p                                                                                                   |
+-----------------------------------------------------------------------------------------------------+
| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_49")<-[:with_genre@0 {}]-("1891")>       |
| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_17")<-[:with_genre@0 {}]-("1891")>       |
| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_10281")<-[:with_genre@0 {}]-("1891")>    |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_4")<-[:acted_by@0 {}]-("1891")>            |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_3")<-[:acted_by@0 {}]-("1891")>            |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_24342")<-[:acted_by@0 {}]-("1891")>        |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_2")<-[:acted_by@0 {}]-("1891")>            |
| <("u_124")-[:watched@0 {}]->("832")-[:with_genre@0 {}]->("g_1110")<-[:with_genre@0 {}]-("1891")>    |
| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_1110")<-[:with_genre@0 {}]-("1891")>     |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_13463")<-[:acted_by@0 {}]-("1891")>        |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_12248")<-[:acted_by@0 {}]-("1891")>        |
| <("u_124")-[:watched@0 {}]->("47981")-[:with_genre@0 {}]->("g_10219")<-[:with_genre@0 {}]-("1891")> |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_6")<-[:acted_by@0 {}]-("1891")>            |
| <("u_124")-[:watched@0 {}]->("497")-[:with_genre@0 {}]->("g_104")<-[:with_genre@0 {}]-("1891")>     |
| <("u_124")-[:watched@0 {}]->("120880")-[:with_genre@0 {}]->("g_104")<-[:with_genre@0 {}]-("1891")>  |
| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_104")<-[:with_genre@0 {}]-("1891")>      |
| <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_130")<-[:acted_by@0 {}]-("1891")>          |
| <("u_124")-[:watched@0 {}]->("497")-[:with_genre@0 {}]->("g_50")<-[:with_genre@0 {}]-("1891")>      |
| <("u_124")-[:watched@0 {}]->("11635")-[:with_genre@0 {}]->("g_50")<-[:with_genre@0 {}]-("1891")>    |
| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_50")<-[:with_genre@0 {}]-("1891")>       |
+-----------------------------------------------------------------------------------------------------+
Got 20 rows (time spent 267151/278139 us)

Wed, 09 Nov 2022 19:05:56 CST

咱们在后果可视化中能够很容易看出这个举荐的后果能够是:

  • 已经喜爱的星战电影的大部分演职人员都也参加了这部和同样是“奥斯卡获奖”且“经典”的电影。

总结

图数据库可作为举荐零碎中信息的最终模式,只管在很多举荐实现中,图库不肯定是最终落地零碎计划的抉择,但图数据库所带来的可视化洞察的后劲还是十分大的。

同时,构建的综合常识图谱上的解释、推理能力与一些实时要求高的图办法中,比方:GNN 的基于模型办法上能起到带来举世无双的作用。

延长浏览

  • Content-based Recommender Systems: State of the Art and Trends
  • A DYNAMIC COLLABORATIVE FILTERING SYSTEM VIA A WEIGHTED CLUSTERING APPROACH
  • Nebula-UP:https://github.com/wey-gu/nebula-up
  • 数据集仓库:https://github.com/wey-gu/movie-recommendation-dataset
  • Jaccard Index:https://en.wikipedia.org/wiki/Jaccard_index
  • Pearson Correlation Coefficient:https://en.wikipedia.org/wiki/Pearson_correlation_coefficient
  • DGL 风行的图深度学习矿建,我的项目官网:https://www.dgl.ai
  • Nebula-DGL:https://github.com/wey-gu/nebula-dgl
  • PinSAGE:https://arxiv.org/abs/1806.01973
  • 美团案例分享:[https://tech.meituan.com/2021…](https://tech.meituan.com/2021…
    )

谢谢你读完本文 (///▽///)

要来近距离体验一把图数据库吗?当初能够用用 NebulaGraph Cloud 来搭建本人的图数据系统哟,快来节俭大量的部署安装时间来搞定业务吧~ NebulaGraph 阿里云计算巢现 30 天收费应用中,点击链接来用用图数据库吧~

想看源码的小伙伴能够返回 GitHub 浏览、应用、(^з^)-☆ star 它 -> GitHub;和其余的 NebulaGraph 用户一起交换图数据库技术和利用技能,留下「你的名片」一起游玩呢~

正文完
 0