HashGNN
此功能处于 Beta 阶段。有关功能阶段的更多信息,请参阅API 阶段。
词汇表
- 有向
-
有向特性。该算法在有向图上定义良好。
- 有向
-
有向特性。该算法忽略图的方向。
- 有向
-
有向特性。该算法不在有向图上运行。
- 无向
-
无向特性。该算法在无向图上定义良好。
- 无向
-
无向特性。该算法忽略图的无向性。
- 异构节点
-
异构节点完全支持。该算法能够区分不同类型的节点。
- 异构节点
-
异构节点允许。该算法无论节点的标签如何,都将所有选定节点视为相似。
- 异构关系
-
异构关系完全支持。该算法能够区分不同类型的关系。
- 异构关系
-
异构关系允许。该算法无论关系的类型如何,都将所有选定关系视为相似。
- 加权关系
-
加权特性。该算法支持将关系属性用作权重,通过relationshipWeightProperty配置参数指定。
- 加权关系
-
加权特性。该算法将每个关系视为同等重要,忽略任何关系权重的值。
HashGNN 在端到端示例 Jupyter 笔记本中有所介绍 |
介绍
HashGNN 是一种节点嵌入算法,它类似于图神经网络(GNN),但不需要模型或训练。GNN 的神经网络被随机哈希函数取代,类似于 `min-hash` 局部敏感哈希。因此,HashGNN 结合了 GNN 和快速随机算法的思想。
HashGNN 的 GDS 实现基于论文“Hashing-Accelerated Graph Neural Networks for Link Prediction”,并进一步引入了一些改进和泛化。泛化包括支持嵌入异构图;不同类型的关系与不同的哈希函数相关联,这允许保留关系类型的图拓扑。此外,可以通过 `neighborInfluence` 配置如何使用邻居节点的特征更新嵌入,以及如何使用同一节点的特征进行更新。
该算法的运行时间通常比 GNN 短得多,但对于某些图,它可以提供与原始论文中所示相当的嵌入质量。此外,与论文“Graph Transformer Networks”在相同数据集上进行基准测试时,异构泛化也给出了可比的结果。
执行不需要 GPU,而 GNN 通常需要 GPU,并且可以在许多 CPU 核上很好地并行化。
算法
为了阐明 HashGNN 的工作原理,我们将通过一个虚拟示例如下的三节点图,供那些对特征选择细节感到好奇并更喜欢从示例中学习的读者参考。
HashGNN 算法只能在二元特征上运行。因此,算法中有一个可选的第一步,用于将(可能非二元的)输入特征转换为二元特征。
对于多次迭代,使用前一次迭代的嵌入为每个节点计算新的二元嵌入。在第一次迭代中,前一次嵌入是输入特征向量或二元化输入向量。
在一次迭代中,每个节点嵌入向量通过取 `K` 个随机样本来构建。随机采样通过连续选择具有最低最小哈希值的特征来执行。节点本身的特征及其邻居的特征都将被考虑。
涉及三种类型的哈希函数:1) 应用于节点自身特征的函数,2) 应用于邻居特征子集的函数,3) 应用于所有邻居特征以选择哈希函数 2) 的子集的函数。对于每次迭代和采样轮次 `k
采样是一致的,即如果节点(a)和(b)具有相同或相似的局部图,则(a)和(b)的样本也相同或相似。局部图是指具有特征和关系类型的子图,包含最多 `iterations` 跳的所有节点。
数字 `K` 在算法配置中称为 `embeddingDensity`。
算法以另一个可选步骤结束,该步骤将二元嵌入映射到密集向量。
特征
原始的 HashGNN 算法假设节点以二元特征作为输入,并生成二元嵌入向量作为输出(除非选择输出稠密化)。由于这在真实世界图中并非总是如此,我们的算法还提供了二元化节点属性或从头生成二元特征的选项。
使用二元节点属性作为特征
如果您的节点属性仅包含 0 或 1 值(或此类值的数组),您可以将它们直接用作 HashGNN 算法的输入。为此,您可以在配置中将它们作为 `featureProperties` 提供。
特征生成
要使用特征生成,请为 `generateFeatures` 配置参数指定一个包含 `dimension` 和 `densityLevel` 的映射。这将生成 `dimension` 数量的特征,其中节点大约有 `densityLevel` 个特征被激活。每个节点的活动特征以均匀随机替换的方式选择。尽管活动特征是随机的,但节点的特征向量充当该节点的近似唯一签名。这类似于节点 ID 的独热编码,但近似在于其维度远低于图的节点计数。请注意,在使用特征生成时,不支持提供任何 `featureProperties`,否则它是强制性的。
特征二值化
特征二值化使用超平面舍入,并通过 `featureProperties` 和一个包含 `threshold` 和 `dimension` 的映射参数 `binarizeFeatures` 进行配置。超平面舍入使用由填充高斯随机值的向量定义的超平面。`dimension` 参数确定输入特征将被转换为的二值特征的数量。对于每个超平面(每个 `dimension` 一个)和节点,我们计算节点的输入特征向量和超平面的法向量的点积。如果此点积大于给定的 `threshold`,则节点将获得与该超平面对应的特征。
尽管超平面舍入可以应用于二值输入,但通常最好直接使用已有的二值输入。然而,有时使用与输入特征数量不同的 `dimension` 进行二值化可能有助于降维或引入 HashGNN 可以利用的冗余。
如果输入特征的量级不同,超平面舍入可能无法很好地工作,因为量级较大的特征将对生成的二值特征影响更大。如果这不是您的应用程序的预期行为,我们建议在运行 HashGNN 之前使用缩放属性或另一种类似方法对节点属性(按特征维度)进行归一化。 |
邻居影响
参数 `neighborInfluence` 决定了算法在选择邻居特征而非同一节点特征方面的倾向性。`neighborInfluence` 的默认值为 `1.0`,在此值下,平均而言,特征将有 `50%` 的时间从邻居处选择。增加该值会导致邻居被选择的频率更高。选择邻居特征的概率作为 `neighborInfluence` 的函数具有曲棍球棒状的形状,有点类似于 `y=log(x)` 或 `y=C - 1/x` 的形状。这意味着概率对 `neighborInfluence` 的低值更敏感。
异构性支持
HashGNN 的 GDS 实现提供了一种新的异构图泛化,它能够区分不同关系类型。要启用异构支持,请将 `heterogeneous` 设置为 true。这种泛化与原始 HashGNN 算法的工作方式相同,但是每当哈希函数应用于邻居节点的特征时,该算法使用的哈希函数不仅取决于迭代和数字 `k < embeddingDensity`,还取决于连接到邻居的关系类型。考虑一个例子,其中 HashGNN 运行一次迭代,我们有 `(a)-[:R]→(x)`、`(b)-[:R]→(x)` 和 `(c)-[:S]→(x)`。假设 `(x)` 的特征 `f` 被选择用于 `(a)`,并且哈希值非常小。这将使得该特征也很可能被 `(b)` 选择。然而,当考虑关系 `(c)-[:S]→(x)` 时,`f` 被 `(c)` 选择不会有任何相关性,因为对 `S` 使用了不同的哈希函数。我们可以得出结论,具有相似邻居(包括节点属性和关系类型)的节点会得到相似的嵌入,而邻居相似度较低的节点会得到相似度较低的嵌入。
运行异构 HashGNN 相对于运行同构嵌入(如 FastRP)的优势在于,无需手动选择多个投影或在这些多个图上运行 FastRP 之前创建元路径图。通过异构算法,完整的异构图可以在一次执行中被使用。
异构图的节点属性模式
异构图通常为不同的节点标签具有不同的节点属性。HashGNN 假定所有节点都具有相同的允许特征。因此,在每个图投影中都使用默认值 `0`。这在二进制输入情况和应用二值化时都有效,因为具有值为 `0` 的二进制特征表现得好像没有该特征一样。`0` 值以稀疏格式表示,因此为许多节点存储 `0` 值的内存开销很小。
方向
在创建图时选择正确的方向可能会产生很大的影响。HashGNN 适用于任何方向,并且方向的选择是特定于问题的。给定一个有向关系类型,您可以选择一个方向,或者使用两个带有 `NATURAL` 和 `REVERSE` 的投影。使用 GNN 的类比,对反向关系使用不同的关系类型会导致在考虑关系与反向关系时使用一组不同的权重。对于 HashGNN,这意味着使用不同的最小哈希函数来处理这两个关系。例如,在引文网络中,一篇论文引用另一篇论文与该论文被引用非常不同。
输出稠密化
由于二进制嵌入需要比密集浮点嵌入更高的维度才能编码相同的信息量,因此二进制嵌入需要更多的内存和更长的下游模型训练时间。输出嵌入可以选择性地通过随机投影进行稠密化,类似于 FastRP 初始化节点属性的方式。通过指定 `outputDimension` 即可激活此行为。输出稠密化可以提高下游任务的运行时间和内存,但代价是由于投影的随机性而引入近似误差。`outputDimension` 越大,近似误差和性能节省越低。
在机器学习管道中的使用
将 HashGNN 生成的节点嵌入作为机器学习管道中的节点属性步骤可能很有用(例如链接预测管道和节点属性预测)。由于 HashGNN 是一种随机算法,并且只有在给定 `featureProperties` 和 `randomSeed` 时才归纳,因此需要注意一些事项。
为了使机器学习模型能够做出有用的预测,重要的是在预测期间生成的特征与在模型训练期间生成的特征具有相似的分布。此外,添加到管道中的节点属性步骤(无论是 HashGNN 还是其他)在训练期间和训练模型的预测期间都会执行。因此,当管道包含一个在训练和预测期间产生差异过大的嵌入的嵌入步骤时,就会出现问题。
这对于如何将 HashGNN 用作节点属性步骤有一些影响。通常,如果使用 HashGNN 作为节点属性步骤在某个图“g”上训练管道,则生成的训练模型应仅应用于与“g”不太相似的图。
如果使用特征生成,则运行预测的图中的大多数节点必须与训练期间使用的原始图“g”中的节点(在数据库意义上)相同。原因是 HashGNN 随机生成节点特征,在这种情况下,它是根据节点来自 Neo4j 数据库中的 ID 进行种子设定的。
如果未使用特征生成(给定 `featureProperties`),则随机初始节点嵌入仅来源于节点属性向量,因此没有基于节点 ID 的随机种子设定。
此外,为了使 HashGNN 消息传递的特征传播在运行之间(训练和预测调用)保持一致,在将 HashGNN 节点属性步骤添加到训练管道时必须提供 `randomSeed` 配置参数的值。
调优算法参数
为了提高 HashGNN 在您的图上的嵌入质量,可以调整算法参数。寻找适合您的特定用例和图的最佳参数的过程通常被称为超参数调优。我们将逐一介绍每个配置参数并解释它们的行为方式。
迭代
节点与其他节点之间影响其嵌入的最大跳数等于 HashGNN 的迭代次数,通过 `iterations` 配置。这类似于 GNN 中的层数或 FastRP 中的迭代次数。通常,`2` 到 `4` 的值就足够了,但有时更多的迭代也很有用。
嵌入密度
`embeddingDensity` 参数是原始论文中用 `k` 表示的参数。对于 HashGNN 的每次迭代,从前一次迭代的嵌入中为同一节点及其邻居选择 `k` 个特征。选定的特征表示为一个集合,因此不同的选定特征的数量可能小于 `k`。此参数设置得越高,算法运行时间越长,并且运行时间呈线性增长。在很大程度上,较高的值会产生更好的嵌入。作为一项粗略的指导方针,可以将 `embeddingDensity` 设置为 128、256、512,或大约嵌入维度的 25%-50%,即二元特征的数量。
特征生成
当应用特征生成时,`dimension` 参数决定了二元特征的数量。高维增加了表达能力,但需要更多数据才能有用,并可能导致下游机器学习任务的维度灾难。此外,还需要更多的计算资源。然而,二元嵌入每维只有一位信息。相比之下,密集 `Float` 嵌入每维有 64 位信息。因此,为了获得与生成密集嵌入的算法(例如 FastRP 或 GraphSAGE)相似的良好 HashGNN 嵌入,通常需要显着更高的维度。`densityLevel` 可以考虑尝试的值包括非常低的值,例如 `1` 或 `2`,或根据需要增加。
特征二值化
当应用二值化时,`dimension` 参数决定了二值特征的数量。高维增加了表达能力,但也增加了特征的稀疏性。因此,更高的维度也应该与更高的 `embeddingDensity` 和/或更低的 `threshold` 结合使用。更高的维度还会导致下游模型的训练时间更长和内存占用更高。增加阈值会导致更稀疏的特征向量。
然而,二元嵌入每维只包含一位信息。相比之下,密集 `Float` 嵌入每维包含 64 位信息。因此,为了使用 HashGNN 获得与使用生成密集嵌入的算法(例如 FastRP 或 GraphSAGE)同样好的嵌入,通常需要显著更高的维度。
默认阈值 `0` 导致每个节点有相当多的特征处于活动状态。通常稀疏特征向量更好,因此增加阈值超过默认值可能很有用。选择一个好阈值的一种启发式方法是基于使用超平面点积的平均值和标准差以及节点特征向量。例如,可以将阈值设置为平均值加上两倍标准差。要获得这些值,请运行 HashGNN 并查看数据库日志,从中读取它们。然后,您可以使用这些值相应地重新配置阈值。
邻居影响
如上所述,默认值是一个合理的起点。如果使用超参数调优库,此参数可以有利地通过具有递增导数的函数(例如指数函数)或 `a/(b - x)` 类型的函数进行转换。从不同节点选择(并在整个迭代中保留)特征的概率取决于 `neighborInfluence` 和到节点的跳数。因此,当 `iterations` 改变时,`neighborInfluence` 应该重新调整。
异构
通常,在异构图中,存储包含多种关系类型的路径需要大量信息,因此对于多次迭代和多种关系类型,可能需要非常高的嵌入维度。对于 HashGNN 等无监督嵌入算法尤其如此。因此,在异构模式下使用多次迭代时应谨慎。
随机种子
随机种子在此算法中具有特殊作用。除了使算法的所有步骤确定性外,`randomSeed` 参数还决定了算法内部使用哪些(在某种程度上)哈希函数。这很重要,因为它极大地影响了每次迭代中采样的特征。哈希在图神经网络的每一层中扮演着与(通常是神经)转换类似的角色,这告诉我们哈希函数有多么重要。事实上,当配置中只有 `randomSeed` 不同时,人们通常会看到算法输出的节点嵌入质量存在显著差异。
由于这些原因,实际调整随机种子参数是有意义的。请注意,应将其作为分类(即非序数)数字进行调整,这意味着值 1 和 2 可以被认为与 1 和 100 相似或不同。开始这样做的一个好方法是选择 5-10 个任意整数(例如,值 1、2、3、4 和 5)作为随机种子的候选。
`randomSeed` 与多个配置参数协同依赖,特别是与 `neighborInfluence` 参数直接影响使用的哈希函数。因此,如果 `neighborInfluence` 发生变化,`randomSeed` 参数很可能需要重新调整。
语法
本节介绍 HashGNN 算法在每种执行模式下使用的语法。我们正在描述语法的命名图变体。要了解有关通用语法变体的更多信息,请参阅语法概述。
CALL gds.hashgnn.stream(
graphName: String,
configuration: Map
) YIELD
nodeId: Integer,
embedding: List of Float
名称 | 类型 | 默认值 | 可选 | 描述 |
---|---|---|---|---|
graphName |
字符串 |
|
否 |
存储在目录中的图的名称。 |
configuration |
映射 |
|
是 |
算法特定和/或图过滤的配置。 |
名称 | 类型 | 默认值 | 可选 | 描述 |
---|---|---|---|---|
字符串列表 |
|
是 |
使用给定的节点标签过滤命名图。将包含具有任何给定标签的节点。 |
|
字符串列表 |
|
是 |
使用给定的关系类型过滤命名图。将包含具有任何给定类型的关系。 |
|
整数 |
|
是 |
用于运行算法的并发线程数。 |
|
字符串 |
|
是 |
可以提供用于更轻松地跟踪算法进度的 ID。 |
|
布尔值 |
|
是 |
如果禁用,则不会记录进度百分比。 |
|
featureProperties |
字符串列表 |
|
是 |
应作为输入特征的节点属性的名称。所有属性名称必须存在于投影图中,并且类型为 Float 或 List of Float。 |
iterations |
整数 |
|
否 |
运行 HashGNN 的迭代次数。必须至少为 1。 |
embeddingDensity |
整数 |
|
否 |
每次迭代中每个节点采样的特征数量。在原始论文中称为 `K`。必须至少为 1。 |
heterogeneous |
布尔值 |
|
是 |
是否应区别对待不同关系类型。 |
neighborInfluence |
浮点数 |
|
是 |
控制每次迭代中邻居特征相对于节点自身特征的采样频率。必须为非负数。 |
binarizeFeatures |
映射 |
|
是 |
一个包含 `dimension` 和 `threshold` 键的映射。如果给定,特征通过超平面舍入转换为 `dimension` 个二值特征。增加 `threshold` 会使输出更稀疏,默认值为 `0`。`dimension` 的值必须至少为 1。 |
generateFeatures |
映射 |
|
是 |
一个包含 `dimension` 和 `densityLevel` 键的映射。当且仅当 `featureProperties` 为空时才应给定。如果给定,将生成 `dimension` 个二值特征,每个节点大约有 `densityLevel` 个活动特征。两者都必须至少为 1,并且 `densityLevel` 最多为 `dimension`。 |
outputDimension |
整数 |
|
是 |
如果给定,嵌入将随机投影到 `outputDimension` 个密集特征中。必须至少为 1。 |
randomSeed |
整数 |
|
是 |
一个随机种子,用于计算嵌入中的所有随机性。 |
名称 | 类型 | 描述 |
---|---|---|
nodeId |
整数 |
节点 ID。 |
embedding |
浮点数列表 |
HashGNN 节点嵌入。 |
CALL gds.hashgnn.mutate(
graphName: String,
configuration: Map
) YIELD
nodeCount: Integer,
nodePropertiesWritten: Integer,
preProcessingMillis: Integer,
computeMillis: Integer,
mutateMillis: Integer,
configuration: Map
名称 | 类型 | 默认值 | 可选 | 描述 |
---|---|---|---|---|
graphName |
字符串 |
|
否 |
存储在目录中的图的名称。 |
configuration |
映射 |
|
是 |
算法特定和/或图过滤的配置。 |
名称 | 类型 | 默认值 | 可选 | 描述 |
---|---|---|---|---|
mutateProperty |
字符串 |
|
否 |
要写入嵌入的 GDS 图中的节点属性。 |
字符串列表 |
|
是 |
使用给定的节点标签过滤命名图。 |
|
字符串列表 |
|
是 |
使用给定的关系类型过滤命名图。 |
|
整数 |
|
是 |
用于运行算法的并发线程数。 |
|
字符串 |
|
是 |
可以提供用于更轻松地跟踪算法进度的 ID。 |
|
featureProperties |
字符串列表 |
|
是 |
应作为输入特征的节点属性的名称。所有属性名称必须存在于投影图中,并且类型为 Float 或 List of Float。 |
iterations |
整数 |
|
否 |
运行 HashGNN 的迭代次数。必须至少为 1。 |
embeddingDensity |
整数 |
|
否 |
每次迭代中每个节点采样的特征数量。在原始论文中称为 `K`。必须至少为 1。 |
heterogeneous |
布尔值 |
|
是 |
是否应区别对待不同关系类型。 |
neighborInfluence |
浮点数 |
|
是 |
控制每次迭代中邻居特征相对于节点自身特征的采样频率。必须为非负数。 |
binarizeFeatures |
映射 |
|
是 |
一个包含 `dimension` 和 `threshold` 键的映射。如果给定,特征通过超平面舍入转换为 `dimension` 个二值特征。增加 `threshold` 会使输出更稀疏,默认值为 `0`。`dimension` 的值必须至少为 1。 |
generateFeatures |
映射 |
|
是 |
一个包含 `dimension` 和 `densityLevel` 键的映射。当且仅当 `featureProperties` 为空时才应给定。如果给定,将生成 `dimension` 个二值特征,每个节点大约有 `densityLevel` 个活动特征。两者都必须至少为 1,并且 `densityLevel` 最多为 `dimension`。 |
outputDimension |
整数 |
|
是 |
如果给定,嵌入将随机投影到 `outputDimension` 个密集特征中。必须至少为 1。 |
randomSeed |
整数 |
|
是 |
一个随机种子,用于计算嵌入中的所有随机性。 |
名称 | 类型 | 描述 |
---|---|---|
nodeCount |
整数 |
已处理的节点数量。 |
nodePropertiesWritten |
整数 |
已写入的节点属性数量。 |
preProcessingMillis |
整数 |
预处理图的毫秒数。 |
computeMillis |
整数 |
运行算法的毫秒数。 |
mutateMillis |
整数 |
将属性添加到内存图的毫秒数。 |
configuration |
映射 |
用于运行算法的配置。 |
CALL gds.hashgnn.write(
graphName: String,
configuration: Map
) YIELD
nodeCount: Integer,
nodePropertiesWritten: Integer,
preProcessingMillis: Integer,
computeMillis: Integer,
writeMillis: Integer,
configuration: Map
名称 | 类型 | 默认值 | 可选 | 描述 |
---|---|---|---|---|
graphName |
字符串 |
|
否 |
存储在目录中的图的名称。 |
configuration |
映射 |
|
是 |
算法特定和/或图过滤的配置。 |
名称 | 类型 | 默认值 | 可选 | 描述 |
---|---|---|---|---|
字符串列表 |
|
是 |
使用给定的节点标签过滤命名图。将包含具有任何给定标签的节点。 |
|
字符串列表 |
|
是 |
使用给定的关系类型过滤命名图。将包含具有任何给定类型的关系。 |
|
整数 |
|
是 |
用于运行算法的并发线程数。 |
|
字符串 |
|
是 |
可以提供用于更轻松地跟踪算法进度的 ID。 |
|
布尔值 |
|
是 |
如果禁用,则不会记录进度百分比。 |
|
整数 |
|
是 |
用于将结果写入 Neo4j 的并发线程数。 |
|
字符串 |
|
否 |
要写入嵌入的 Neo4j 数据库中的节点属性。 |
|
featureProperties |
字符串列表 |
|
是 |
应作为输入特征的节点属性的名称。所有属性名称必须存在于投影图中,并且类型为 Float 或 List of Float。 |
iterations |
整数 |
|
否 |
运行 HashGNN 的迭代次数。必须至少为 1。 |
embeddingDensity |
整数 |
|
否 |
每次迭代中每个节点采样的特征数量。在原始论文中称为 `K`。必须至少为 1。 |
heterogeneous |
布尔值 |
|
是 |
是否应区别对待不同关系类型。 |
neighborInfluence |
浮点数 |
|
是 |
控制每次迭代中邻居特征相对于节点自身特征的采样频率。必须为非负数。 |
binarizeFeatures |
映射 |
|
是 |
一个包含 `dimension` 和 `threshold` 键的映射。如果给定,特征通过超平面舍入转换为 `dimension` 个二值特征。增加 `threshold` 会使输出更稀疏,默认值为 `0`。`dimension` 的值必须至少为 1。 |
generateFeatures |
映射 |
|
是 |
一个包含 `dimension` 和 `densityLevel` 键的映射。当且仅当 `featureProperties` 为空时才应给定。如果给定,将生成 `dimension` 个二值特征,每个节点大约有 `densityLevel` 个活动特征。两者都必须至少为 1,并且 `densityLevel` 最多为 `dimension`。 |
outputDimension |
整数 |
|
是 |
如果给定,嵌入将随机投影到 `outputDimension` 个密集特征中。必须至少为 1。 |
randomSeed |
整数 |
|
是 |
一个随机种子,用于计算嵌入中的所有随机性。 |
名称 | 类型 | 描述 |
---|---|---|
nodeCount |
整数 |
已处理的节点数量。 |
nodePropertiesWritten |
整数 |
已写入的节点属性数量。 |
preProcessingMillis |
整数 |
预处理图的毫秒数。 |
computeMillis |
整数 |
运行算法的毫秒数。 |
writeMillis |
整数 |
写回结果的毫秒数。 |
configuration |
映射 |
用于运行算法的配置。 |
示例
下面的所有示例都应在空数据库中运行。 示例使用Cypher 投影作为规范。原生投影将在未来版本中弃用。 |
在本节中,我们将展示在具体图上运行 HashGNN 节点嵌入算法的示例。目的是说明结果是什么样子,并提供如何在实际设置中使用该算法的指南。我们将在一个包含少量节点以特定模式连接的小型社交网络图上进行此操作。
CREATE
(dan:Person {name: 'Dan', age: 18, experience: 63, hipster: 0}),
(annie:Person {name: 'Annie', age: 12, experience: 5, hipster: 0}),
(matt:Person {name: 'Matt', age: 22, experience: 42, hipster: 0}),
(jeff:Person {name: 'Jeff', age: 51, experience: 12, hipster: 0}),
(brie:Person {name: 'Brie', age: 31, experience: 6, hipster: 0}),
(elsa:Person {name: 'Elsa', age: 65, experience: 23, hipster: 1}),
(john:Person {name: 'John', age: 4, experience: 100, hipster: 0}),
(apple:Fruit {name: 'Apple', tropical: 0, sourness: 0.3, sweetness: 0.6}),
(banana:Fruit {name: 'Banana', tropical: 1, sourness: 0.1, sweetness: 0.9}),
(mango:Fruit {name: 'Mango', tropical: 1, sourness: 0.3, sweetness: 1.0}),
(plum:Fruit {name: 'Plum', tropical: 0, sourness: 0.5, sweetness: 0.8}),
(dan)-[:LIKES]->(apple),
(annie)-[:LIKES]->(banana),
(matt)-[:LIKES]->(mango),
(jeff)-[:LIKES]->(mango),
(brie)-[:LIKES]->(banana),
(elsa)-[:LIKES]->(plum),
(john)-[:LIKES]->(plum),
(dan)-[:KNOWS]->(annie),
(dan)-[:KNOWS]->(matt),
(annie)-[:KNOWS]->(matt),
(annie)-[:KNOWS]->(jeff),
(annie)-[:KNOWS]->(brie),
(matt)-[:KNOWS]->(brie),
(brie)-[:KNOWS]->(elsa),
(brie)-[:KNOWS]->(jeff),
(john)-[:KNOWS]->(jeff);
此图代表七个互相认识的人。
在 Neo4j 中有了图之后,我们现在可以将其投影到图目录中,为算法执行做好准备。我们使用 Cypher 投影,以 `Person` 节点和 `KNOWS` 关系为目标。对于关系,我们将使用 `UNDIRECTED` 方向。
MATCH (source)
OPTIONAL MATCH (source)-[r]->(target)
RETURN gds.graph.project(
'persons',
source,
target,
{
sourceNodeLabels: labels(source),
targetNodeLabels: labels(target),
sourceNodeProperties: {
age: coalesce(source.age, 0.0),
experience: coalesce(source.experience, 0.0),
hipster: coalesce(source.hipster, 0.0),
tropical: coalesce(source.tropical, 0.0),
sourness: coalesce(source.sourness, 0.0),
sweetness: coalesce(source.sweetness, 0.0)
},
targetNodeProperties: {
age: coalesce(target.age, 0.0),
experience: coalesce(target.experience, 0.0),
hipster: coalesce(target.hipster, 0.0),
tropical: coalesce(target.tropical, 0.0),
sourness: coalesce(target.sourness, 0.0),
sweetness: coalesce(target.sweetness, 0.0)
},
relationshipType: type(r)
},
{ undirectedRelationshipTypes: ['KNOWS', 'LIKES'] }
)
由于我们将在某些示例中使用二值化,并且属性具有不同的比例,因此我们将创建 `experience` 属性的比例版本。
CALL gds.scaleProperties.mutate('persons', {
nodeProperties: ['experience'],
scaler: 'Minmax',
mutateProperty: 'experience_scaled'
}) YIELD nodePropertiesWritten
内存估算
首先,我们将使用 `estimate` 过程估算运行算法的成本。这可以通过任何执行模式完成。在此示例中,我们将使用 `stream` 模式。估算算法有助于了解运行算法对图的内存影响。当您随后实际在一种执行模式下运行算法时,系统将执行估算。如果估算显示执行超出其内存限制的概率非常高,则禁止执行。要了解更多信息,请参阅自动估算和执行阻止。
有关 `estimate` 的更多详细信息,请参阅内存估算。
CALL gds.hashgnn.stream.estimate('persons', {nodeLabels: ['Person'], iterations: 3, embeddingDensity: 2, binarizeFeatures: {dimension: 4, threshold: 0}, featureProperties: ['age', 'experience']})
YIELD nodeCount, relationshipCount, bytesMin, bytesMax, requiredMemory
nodeCount | relationshipCount | bytesMin | bytesMax | requiredMemory |
---|---|---|---|---|
7 |
18 |
2056 |
2056 |
"2056 字节" |
流
在 `stream` 执行模式下,算法返回每个节点的嵌入。这允许我们直接检查结果或在 Cypher 中进行后处理,而没有任何副作用。例如,我们可以收集结果并将其传递给相似性算法。
有关 `stream` 模式的更多详细信息,请参阅流。
CALL gds.hashgnn.stream('persons',
{
nodeLabels: ['Person'],
iterations: 1,
embeddingDensity: 2,
binarizeFeatures: {dimension: 4, threshold: 32},
featureProperties: ['age', 'experience'],
randomSeed: 42
}
)
YIELD nodeId, embedding
RETURN gds.util.asNode(nodeId).name AS person, embedding
ORDER BY person
person | embedding |
---|---|
"Annie" |
[1.0, 0.0, 1.0, 0.0] |
"Brie" |
[1.0, 0.0, 0.0, 0.0] |
"Dan" |
[0.0, 1.0, 0.0, 0.0] |
"Elsa" |
[1.0, 0.0, 1.0, 0.0] |
"Jeff" |
[1.0, 0.0, 1.0, 0.0] |
"John" |
[1.0, 1.0, 0.0, 0.0] |
"Matt" |
[1.0, 1.0, 0.0, 0.0] |
算法的结果并不容易直观理解,因为节点嵌入格式是节点在其邻域内的数学抽象,专为机器学习程序设计。我们可以看到,嵌入有四个元素(根据 `binarizeFeatures.dimension` 配置)。
由于算法的随机性,除非指定了 `randomSeed`,否则结果将在不同运行之间有所不同。 |
CALL gds.hashgnn.stream('persons',
{
nodeLabels: ['Person'],
iterations: 1,
embeddingDensity: 2,
featureProperties: ['hipster'],
randomSeed: 123
}
)
YIELD nodeId, embedding
RETURN gds.util.asNode(nodeId).name AS person, embedding
ORDER BY person
person | embedding |
---|---|
"Annie" |
[0.0] |
"Brie" |
[1.0] |
"Dan" |
[0.0] |
"Elsa" |
[1.0] |
"Jeff" |
[0.0] |
"John" |
[0.0] |
"Matt" |
[0.0] |
在此示例中,嵌入维度变为 `1`,因为没有二值化,特征数量为 `1`,这是由于单个“hipster”属性。
CALL gds.hashgnn.stream('persons',
{
nodeLabels: ['Person'],
iterations: 1,
embeddingDensity: 2,
generateFeatures: {dimension: 6, densityLevel: 1},
randomSeed: 42
}
)
YIELD nodeId, embedding
RETURN gds.util.asNode(nodeId).name AS person, embedding
ORDER BY person
person | embedding |
---|---|
"Annie" |
[0.0, 0.0, 1.0, 0.0, 1.0, 0.0] |
"Brie" |
[0.0, 0.0, 0.0, 0.0, 1.0, 0.0] |
"Dan" |
[0.0, 0.0, 1.0, 0.0, 0.0, 0.0] |
"Elsa" |
[0.0, 0.0, 1.0, 0.0, 0.0, 0.0] |
"Jeff" |
[0.0, 0.0, 0.0, 1.0, 1.0, 0.0] |
"John" |
[0.0, 0.0, 0.0, 0.0, 1.0, 0.0] |
"Matt" |
[0.0, 0.0, 0.0, 0.0, 1.0, 0.0] |
而且正如我们所见,每个节点至少有一个活动特征。密度约为 50%,并且没有节点具有超过两个活动特征(受 `embeddingDensity` 限制)。
CALL gds.hashgnn.stream('persons',
{
heterogeneous: true,
iterations: 2,
embeddingDensity: 4,
binarizeFeatures: {dimension: 6, threshold: 0.2},
featureProperties: ['experience_scaled', 'sourness', 'sweetness', 'tropical'],
randomSeed: 42
}
)
YIELD nodeId, embedding
RETURN gds.util.asNode(nodeId).name AS name, embedding
ORDER BY name
名称 | embedding |
---|---|
"Annie" |
[1.0, 1.0, 1.0, 0.0, 0.0, 0.0] |
"Apple" |
[1.0, 0.0, 1.0, 0.0, 0.0, 0.0] |
"Banana" |
[1.0, 0.0, 0.0, 0.0, 0.0, 1.0] |
"Brie" |
[1.0, 1.0, 1.0, 0.0, 0.0, 0.0] |
"Dan" |
[1.0, 1.0, 0.0, 0.0, 0.0, 1.0] |
"Elsa" |
[1.0, 1.0, 0.0, 0.0, 0.0, 0.0] |
"Jeff" |
[1.0, 0.0, 1.0, 0.0, 0.0, 0.0] |
"John" |
[1.0, 0.0, 0.0, 0.0, 0.0, 1.0] |
"Mango" |
[1.0, 0.0, 0.0, 0.0, 0.0, 1.0] |
"Matt" |
[1.0, 1.0, 1.0, 0.0, 0.0, 0.0] |
"Plum" |
[1.0, 0.0, 1.0, 0.0, 0.0, 0.0] |
CALL gds.hashgnn.stream('persons',
{
heterogeneous: true,
iterations: 2,
embeddingDensity: 4,
binarizeFeatures: {dimension: 6, threshold: 0.2},
featureProperties: ['experience_scaled', 'sourness', 'sweetness', 'tropical'],
outputDimension: 4,
randomSeed: 42
}
)
YIELD nodeId, embedding
RETURN gds.util.asNode(nodeId).name AS name, embedding
ORDER BY name
名称 | embedding |
---|---|
"Annie" |
[0.0, 0.8660253882, -1.7320507765, 0.8660253882] |
"Apple" |
[0.0, 0.0, -1.7320507765, 0.8660253882] |
"Banana" |
[0.0, 0.0, -1.7320507765, 0.8660253882] |
"Brie" |
[0.0, 0.8660253882, -1.7320507765, 0.8660253882] |
"Dan" |
[0.0, 0.8660253882, -1.7320507765, 0.8660253882] |
"Elsa" |
[0.0, 0.8660253882, -0.8660253882, 0.0] |
"Jeff" |
[0.0, 0.0, -1.7320507765, 0.8660253882] |
"John" |
[0.0, 0.0, -1.7320507765, 0.8660253882] |
"Mango" |
[0.0, 0.0, -1.7320507765, 0.8660253882] |
"Matt" |
[0.0, 0.8660253882, -1.7320507765, 0.8660253882] |
"Plum" |
[0.0, 0.0, -1.7320507765, 0.8660253882] |
变异
`mutate` 执行模式扩展了 `stats` 模式,并带有一个重要的副作用:用包含该节点嵌入的新节点属性更新命名图。新属性的名称通过强制配置参数 `mutateProperty` 指定。结果是一行摘要,类似于 `stats`,但包含一些额外的指标。当多个算法结合使用时,`mutate` 模式特别有用。
有关 `mutate` 模式的更多详细信息,请参阅变异。
CALL gds.hashgnn.mutate(
'persons',
{
mutateProperty: 'hashgnn-embedding',
heterogeneous: true,
iterations: 2,
embeddingDensity: 4,
binarizeFeatures: {dimension: 6, threshold: 0.2},
featureProperties: ['experience_scaled', 'sourness', 'sweetness', 'tropical'],
randomSeed: 42
}
)
YIELD nodePropertiesWritten
nodePropertiesWritten |
---|
11 |
图“persons”现在有一个节点属性 `hashgnn-embedding`,它存储了每个节点的节点嵌入。要了解如何检查内存中图的新模式,请参阅列出图。
写入
`write` 执行模式扩展了 `stats` 模式,并具有一个重要的副作用:将每个节点的嵌入作为属性写入 Neo4j 数据库。新属性的名称通过强制配置参数 `writeProperty` 指定。结果是一行摘要,类似于 `stats`,但包含一些额外的指标。`write` 模式能够将结果直接持久化到数据库中。
有关 `write` 模式的更多详细信息,请参阅写入。
CALL gds.hashgnn.write(
'persons',
{
writeProperty: 'hashgnn-embedding',
heterogeneous: true,
iterations: 2,
embeddingDensity: 4,
binarizeFeatures: {dimension: 6, threshold: 0.2},
featureProperties: ['experience_scaled', 'sourness', 'sweetness', 'tropical'],
randomSeed: 42
}
)
YIELD nodePropertiesWritten
nodePropertiesWritten |
---|
11 |
虚拟示例
或许以下示例最好用纸和笔来体会。
假设我们有一个节点 `a`,特征为 `f1`;一个节点 `b`,特征为 `f2`;以及一个节点 `c`,特征为 `f1` 和 `f3`。图结构为 `a—b—c`。我们想象运行 HashGNN 一次迭代,`embeddingDensity=2`。为简单起见,我们假设哈希函数返回一些我们编造的数字。
在第一次迭代和 `k=0` 时,我们计算 `(a)` 的嵌入。`f1` 的哈希值结果为 `7`。由于 `(b)` 是 `(a)` 的邻居,我们为其特征 `f2` 生成一个值,结果为 `11`。值 `7` 从我们称为“一”的哈希函数中采样,`11` 从哈希函数“二”中采样。因此,`f1` 被添加到 `(a)` 的新特征中,因为它具有较小的哈希值。我们对 `k=1` 重复此过程,这次哈希值为 `4` 和 `2`,因此 `f2` 现在被添加到 `(a)` 的特征中。
现在我们考虑 `(b)`。特征 `f2` 使用哈希函数“一”得到哈希值 `8`。查看邻居 `(a)`,我们采样 `f1` 的哈希值,其使用哈希函数“二”得到 `5`。由于 `(c)` 有多个特征,我们还必须在考虑“获胜”特征(作为哈希函数“二”的输入)之前,选择 `f1` 和 `f3` 中的一个。我们为此目的使用第三个哈希函数“三”,`f3` 得到较小的值 `1`。现在我们计算 `f3` 使用“二”的哈希值,它变为 `6`。由于 `5` 小于 `6`,`f1` 是 `(b)` 的“获胜”邻居特征,并且由于 `5` 也小于 `8`,它是总体“获胜”特征。因此,我们将 `f1` 添加到 `(b)` 的嵌入中。我们类似地对 `k=1` 进行操作,`f1` 再次被选中。由于嵌入由二值特征组成,第二次添加没有效果。
我们省略了计算 `(c)` 嵌入的细节。
在两次采样回合之后,迭代完成,由于只有一次迭代,我们完成了。每个节点都有一个二值嵌入,其中包含原始二值特征的某个子集。特别是,`(a)` 具有特征 `f1` 和 `f2`,而 `(b)` 只有特征 `f1`。