端到端工作流程
一个实际的端到端工作流通常涉及按顺序使用多种算法。本示例展示了如何通过以下步骤创建一个基于协同过滤的简单产品推荐引擎:
-
创建产品和客户图。
-
使用 FastRP 算法计算并向图中添加节点嵌入 (node embeddings)。
-
使用 k-最近邻 (kNN) 算法,基于节点嵌入计算每对客户的相似度得分。
-
使用 Cypher 查询寻找相似客户并推荐产品。
创建图
以下 Cypher 查询在 Neo4j 数据库中创建一个产品和客户的示例图。amount 关系属性表示客户在给定产品上每周平均花费的金额。
CREATE
(dan:Person {name: 'Dan'}),
(annie:Person {name: 'Annie'}),
(matt:Person {name: 'Matt'}),
(jeff:Person {name: 'Jeff'}),
(brie:Person {name: 'Brie'}),
(elsa:Person {name: 'Elsa'}),
(cookies:Product {name: 'Cookies'}),
(tomatoes:Product {name: 'Tomatoes'}),
(cucumber:Product {name: 'Cucumber'}),
(celery:Product {name: 'Celery'}),
(kale:Product {name: 'Kale'}),
(milk:Product {name: 'Milk'}),
(chocolate:Product {name: 'Chocolate'}),
(dan)-[:BUYS {amount: 1.2}]->(cookies),
(dan)-[:BUYS {amount: 3.2}]->(milk),
(dan)-[:BUYS {amount: 2.2}]->(chocolate),
(annie)-[:BUYS {amount: 1.2}]->(cucumber),
(annie)-[:BUYS {amount: 3.2}]->(milk),
(annie)-[:BUYS {amount: 3.2}]->(tomatoes),
(matt)-[:BUYS {amount: 3}]->(tomatoes),
(matt)-[:BUYS {amount: 2}]->(kale),
(matt)-[:BUYS {amount: 1}]->(cucumber),
(jeff)-[:BUYS {amount: 3}]->(cookies),
(jeff)-[:BUYS {amount: 2}]->(milk),
(brie)-[:BUYS {amount: 1}]->(tomatoes),
(brie)-[:BUYS {amount: 2}]->(milk),
(brie)-[:BUYS {amount: 2}]->(kale),
(brie)-[:BUYS {amount: 3}]->(cucumber),
(brie)-[:BUYS {amount: 0.3}]->(celery),
(elsa)-[:BUYS {amount: 3}]->(chocolate),
(elsa)-[:BUYS {amount: 3}]->(milk)
该图如下所示
下一个查询从 Neo4j 图中创建一个名为 purchases 的内存图。与原始数据唯一的区别是丢弃了 :BUYS 关系的方向;这是因为在使用 FastRP 算法时,无向关系是默认选择。
MATCH (source:Person)-[r:BUYS]->(target:Product)
RETURN gds.graph.project(
'purchases',
source,
target,
{
sourceNodeLabels: labels(source),
targetNodeLabels: labels(target),
relationshipType: 'BUYS',
relationshipProperties: r { .amount }
},
{ undirectedRelationshipTypes: ['BUYS'] }
)
将嵌入添加到图中
节点嵌入通常用于捕获图中的拓扑信息以便进行进一步处理,例如供另一个算法使用。
GDS 提供了多种算法来计算嵌入,而 FastRP 是一个很好的入门默认选择。由于嵌入必须在稍后提供给 kNN 算法,因此该算法必须以 mutate 模式运行,以便将它们添加到 purchases 图中。
CALL gds.fastRP.mutate( (1)
'purchases', (2)
{ (3)
embeddingDimension: 4,
iterationWeights: [0.8, 1, 1, 1],
relationshipWeightProperty: 'amount',
randomSeed: 42,
mutateProperty: 'embedding'
}
)
YIELD nodePropertiesWritten
| 1 | 以 mutate 模式运行的 gds.fastRP 算法。 |
| 2 | 在其上运行算法并添加新节点属性的投影图名称。 |
| 3 | 算法语法部分(Mutate 模式面板)中列出的配置参数。此处,embeddingDimension 设置为 4,因为图规模较小;iterationWeights 是凭经验选择的,以产生合理的结果;relationshipWeightProperty 设置为计算邻近嵌入的加权平均值。添加 randomSeed 是为了在每次运行时获得相同的结果,但这对于实际计算并非必需。mutateProperty 是将包含节点嵌入的新节点属性。 |
| nodePropertiesWritten |
|---|
13 |
您可以使用相应的 gds.fastRP.stream 过程并从配置参数中删除 mutateProperty,从而像基础工作流示例那样以 stream 模式运行该算法。
计算并写入相似度
当嵌入作为新的 embedding 节点属性可用时,您可以运行 kNN 算法来计算每对节点之间的相似度得分。以 write 模式运行 kNN 算法,将 score 关系添加到 Neo4j 数据库中,并在 Cypher 查询中使用它。
CALL gds.knn.write( (1)
'purchases', (2)
{ (3)
nodeProperties: ['embedding'],
nodeLabels: ['Person'],
topK: 2,
sampleRate: 1.0,
deltaThreshold: 0.0,
randomSeed: 42,
concurrency: 1,
writeProperty: 'score',
writeRelationshipType: 'SIMILAR'
}
)
YIELD similarityDistribution
RETURN similarityDistribution.mean AS meanSimilarity (4)
| 1 | 以 write 模式运行的 gds.knn 算法。 |
| 2 | 在其上运行算法的投影图名称。write 模式不会更新内存图。 |
| 3 | 算法语法部分(Write 模式面板)中列出的配置参数。nodeLabels 选项设置为 ['Person'],因为我们只对 Person-Person 相似度感兴趣,不想将其与例如 Person-Product 相似度混合。此处,topK 设置为 2,以便仅为每个源节点选择两个最接近的目标,而 sampleRate 和 deltaThreshold 分别设置为 1 和 0,因为图规模很小。设置 concurrency 和 randomSeed 是为了在每次运行时获得相同的结果,但对于实际计算并非必需。两个 write 属性用于写入一个新的 :SIMILAR 关系,其中 score 属性包含两个节点之间的相似度得分。 |
| 4 | mean 是返回的 similarityDistribution 映射的字段之一。 |
| meanSimilarity |
|---|
0.8800284068 |
节点之间的平均相似度很高。这是因为相似度分布是在为每个源节点选择 topK=2 个最相似的目标后计算的。此外,该图有两个用户集群,他们的购买记录非常相似:(Brie, Matt 和 Annie)以及 (Elsa, Dan 和 Jeff)。使用更高的 topK 值将产生更低的平均相似度。
如果我们要检查“人-产品”相似度,可以通过改用过滤 K-最近邻来实现。
寻找最相似的节点
将相似度关系写入 Neo4j 后,您可以使用 Cypher 查找客户对,并按其相似度得分对它们进行排序。
MATCH (n:Person)-[r:SIMILAR]->(m:Person)
RETURN n.name AS person1, m.name AS person2, r.score AS similarity
ORDER BY similarity DESCENDING, person1, person2
| person1 | person2 | similarity |
|---|---|---|
"Dan" |
"Elsa" |
0.9866833091 |
"Elsa" |
"Dan" |
0.9866833091 |
"Brie" |
"Matt" |
0.9740184546 |
"Matt" |
"Brie" |
0.9740184546 |
"Annie" |
"Matt" |
0.9724045992 |
"Matt" |
"Annie" |
0.9724045992 |
"Annie" |
"Brie" |
0.9154552221 |
"Brie" |
"Annie" |
0.9154552221 |
"Jeff" |
"Annie" |
0.8667784333 |
"Jeff" |
"Matt" |
0.7591181397 |
"Dan" |
"Jeff" |
0.6660436392 |
"Elsa" |
"Jeff" |
0.5712890029 |
查询结果显示,名为“Dan”和“Elsa”的节点非常相似。事实上,他们都连接到三个 :Product 节点,其中两个是相同的(名为“Milk”和“Chocolate”的节点),且金额相似。“Cookies”产品仅由 Dan 购买,金额较低,但由于其在图中的邻近性以及部分随机性,它与其他产品也具有一定程度的相似性。
进行推荐
协同过滤的基本假设是,一个客户购买的产品可能对尚未购买它们的相似客户有吸引力。已知“Annie”和“Matt”是相似的,您可以使用 Cypher 查询为他们中的每一个进行产品推荐。
MATCH (:Person {name: "Annie"})-->(p1:Product)
WITH collect(p1) AS products
MATCH (:Person {name: "Matt"})-->(p2:Product)
WHERE NOT p2 IN products
RETURN p2.name AS recommendation
| recommendation |
|---|
"Kale" |
查询查找“Annie”正在购买的产品,然后选择“Matt”正在购买但“Annie”尚未购买的产品。因此,生成的节点“Kale”就是为“Annie”推荐的产品。