快速随机投影 (Fast Random Projection)
简介
快速随机投影(Fast Random Projection,简称 FastRP)是一种属于随机投影算法族的节点嵌入算法。这些算法在理论上由 Johnson-Lindenstrauss 引理支持,根据该引理,人们可以将 n 个 任意 维度的向量投影到 O(log(n)) 维度,并仍然能够近似保留点之间的成对距离。事实上,随机选择的线性投影满足这一性质。
因此,此类技术允许在保留大部分距离信息的同时进行激进的降维。FastRP 算法作用于图,在这种情况下,我们关心的是保留节点与其邻居之间的相似性。这意味着具有相似邻域的两个节点应被分配相似的嵌入向量。相反,不相似的两个节点不应被分配相似的嵌入向量。
用于 Snowflake 的 Neo4j 图分析中的 FastRP 实现通过以下几种方式扩展了原始算法[1]:
-
它允许使用 节点属性 来影响初始随机向量的创建。
-
它引入了
nodeSelfInfluence(节点自身影响)参数。 -
它支持 有向图。
-
它支持 加权图。
FastRP 算法最初使用一种称为 极度稀疏随机投影 的技术为所有节点分配随机向量 [2]。从随机向量(节点投影)开始,并通过对节点邻域进行迭代平均,算法为每个节点 n 构建了一系列 中间嵌入 。更准确地说,
其中 m 为 n 的邻居, 是节点的初始随机向量。
节点 n 的嵌入 是算法的输出,它是上述向量和嵌入的组合:
因此,每个节点的嵌入取决于半径等于迭代次数的邻域。通过这种方式,FastRP 在利用图中高阶关系的同时仍保持了高度的可扩展性。
节点属性
大多数现实世界的图都包含节点属性,这些属性存储有关节点及其代表内容的信息。用于 Snowflake 的 Neo4j 图分析中的 FastRP 算法通过将节点属性纳入考量,扩展了原始的 FastRP 算法。因此,生成的嵌入可以更准确地表示图。
算法的节点属性感知方面通过 featureProperties 和 propertyRatio 参数进行配置。featureProperties 中的每个节点属性都与一个维度为 propertyDimension 的随机生成向量相关联,其中 propertyDimension = embeddingDimension * propertyRatio。然后,每个节点使用由两部分串联而成的 embeddingDimension 大小的向量进行初始化:
-
第一部分的形成方式与标准 FastRP 算法相同。
-
第二部分是属性向量的线性组合,使用节点的属性值作为权重。
然后,算法按照与 FastRP 算法相同的逻辑进行处理。因此,算法将输出大小为 embeddingDimension 的数组。嵌入中的最后 propertyDimension 个坐标捕获有关附近节点属性值的信息(下文称为“属性部分”),而剩余的坐标(embeddingDimension - propertyDimension 个;“拓扑部分”)捕获有关附近节点存在的信息。
[0, 1, ... | ..., N - 1, N]
^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^
topology part | property part
^
property ratio
调整算法参数
为了使用 FastRP 提高图上的嵌入质量,可以对算法参数进行调整。为特定用例和图找到最佳参数的过程通常被称为 超参数优化。我们将逐一介绍配置参数并解释其行为。
为了获得统计上可靠的结果,保留一个从参数调整中排除的测试集是一个好主意。在选择一组参数值后,可以使用下游机器学习任务在测试集上评估嵌入质量。通过改变参数值并研究机器学习任务的精度,可以推导出最适合具体数据集和用例的参数值。要构建这样一个集合,您可能需要在图中使用专门的节点标签来表示不包含测试数据的子图。
嵌入维度
嵌入维度是生成向量的长度。维度越大,精度越高,但运行成本也越高。
最佳嵌入维度取决于图中的节点数量。由于嵌入所能编码的信息量受限于其维度,因此较大的图通常需要较大的嵌入维度。典型值是 128–1024 范围内的 2 的幂。对于 105 数量级的图,至少 256 的值可以获得良好的结果,但通常增加维度会改善结果。然而,增加嵌入维度会线性增加内存需求和运行时间。
归一化强度
归一化强度用于控制节点度数如何影响嵌入。使用负值将降低高频邻居的重要性,而正值将增加其重要性。最佳归一化强度取决于图以及将使用嵌入的任务。在原始论文中,超参数调整是在 [-1,0](无正值)的范围内进行的,但我们发现某些情况下,正的归一化强度能产生更好的结果。
迭代权重
迭代权重参数控制两个方面:迭代次数以及它们对最终节点嵌入的相对影响。该参数是一个数字列表,其中每个数字表示一次迭代,数字本身是该迭代应用的权重。
在每次迭代中,算法将在图中的所有关系上进行扩展。这产生了一些影响:
-
在单次迭代中,每个节点嵌入只考虑直接邻居。
-
在两次迭代中,每个节点嵌入会考虑直接邻居和二阶邻居。
-
在三次迭代中,每个节点嵌入会考虑直接邻居、二阶邻居和三阶邻居。直接邻居可能会在不同的迭代中被触及两次。
-
通常,对应于第
i次迭代的嵌入包含了取决于长度为i的路径可达节点的功能。如果图是无向的,则对于任何整数k,长度为L的路径可达的节点也可以通过L+2k的长度到达。 -
特别是,一个节点可能在每次偶数迭代时回到其自身(取决于图中的方向)。
最好在偶数位置和奇数位置至少有一个非零权重。通常建议至少使用几次迭代,例如三次。但是,过高的值将考虑距离较远的节点,可能不具有信息量,甚至可能有害。这里的直觉是,随着投影距离节点的距离越来越远,邻域的特定性就越低。当然,更多的迭代次数也需要更多的时间来完成。
节点自身影响 (Node Self Influence)
节点自身影响是原始 FastRP 算法的一种变体。
一个节点的嵌入在第 i 次迭代时受中间嵌入影响的程度由 iterationWeights 的第 i 个元素控制。这也可以看作是:从一个节点出发在 i 跳内可达的节点的初始随机向量(或投影)对该节点嵌入的影响程度。类似地,nodeSelfInfluence 的行为就像是第 0 次迭代的迭代权重,或者节点自身的投影对该节点嵌入的影响程度。
将此参数设置为非零值的一个原因是,如果您的图连接度较低或存在大量孤立节点。孤立节点结合 propertyRatio = 0.0 会导致嵌入包含全零。然而,结合节点属性和节点自身影响可以为这些节点产生更有意义的嵌入。这可以看作是在图结构(局部)缺失时生成回退特征。此外,有时节点自身的属性本身就是信息丰富的特征,即使连接度很高,也建议包含在内。最后,节点自身影响可用于纯降维,以压缩用于节点分类的节点属性。
如果不使用节点属性,使用 nodeSelfInfluence 也可能产生积极影响,具体取决于其他设置和问题本身。
语法
本节涵盖了执行 FastRP 算法所使用的语法。
CALL Neo4j_Graph_Analytics.graph.fast_rp(
'CPU_X64_XS', (1)
{
['defaultTablePrefix': '...',] (2)
'project': {...}, (3)
'compute': {...}, (4)
'write': {...} (5)
}
);
| 1 | 计算池选择器。 |
| 2 | 表引用的可选前缀。 |
| 3 | 项目配置。 |
| 4 | 计算配置。 |
| 5 | 写入配置。 |
| 名称 | 类型 | 默认 | 可选 | 描述 |
|---|---|---|---|---|
computePoolSelector |
字符串 |
|
否 |
用于运行 FastRP 作业的计算池选择器。 |
配置 |
Map |
|
否 |
用于图项目、算法计算和结果回写的配置。 |
配置映射由以下三个条目组成。
| 有关以下项目配置的更多详细信息,请参阅 项目文档。 |
| 名称 | 类型 |
|---|---|
nodeTables |
节点表列表。 |
relationshipTables |
关系类型到关系表的映射。 |
| 名称 | 类型 | 默认 | 可选 | 描述 |
|---|---|---|---|---|
resultProperty |
字符串 |
|
是 |
将回写到 Snowflake 数据库的节点属性。 |
propertyRatio |
浮点数 |
|
是 |
属性嵌入维度与总 |
featureProperties |
字符串列表 |
|
是 |
应作为输入特征使用的节点属性名称。所有属性名称必须存在于投影图中,且类型为 Float 或 List of Float。 |
embeddingDimension |
整数 |
|
否 |
计算出的节点嵌入的维度。最小值为 1。 |
iterationWeights |
浮点数列表 |
|
是 |
包含每次迭代的权重。该权重控制该迭代的中间嵌入对最终嵌入的贡献程度。 |
nodeSelfInfluence |
浮点数 |
|
是 |
控制每个节点初始随机向量对其最终嵌入的贡献程度。 |
normalizationStrength |
浮点数 |
|
是 |
每个节点的初始随机向量按其度数的 |
randomSeed |
整数 |
|
是 |
用于计算嵌入中所有随机性的随机种子。 |
relationshipWeightProperty |
字符串 |
|
是 |
用于加权随机投影的关系属性名称。如果未指定,算法将按无权方式运行。 |
迭代次数等于 |
||||
要求 |
||||
| 关于下文写入配置的更多详细信息,请参考 写入文档。 |
| 名称 | 类型 | 默认 | 可选 | 描述 |
|---|---|---|---|---|
nodeLabel |
字符串 |
|
否 |
内存中图中用于写入节点属性的节点标签。 |
nodeProperty |
字符串 |
|
是 |
将回写到 Snowflake 数据库的节点属性。 |
outputTable |
字符串 |
|
否 |
Snowflake 数据库中写入节点属性的表。 |
示例
在本节中,我们将展示在具体图上运行 FastRP 节点嵌入算法的示例。目的是说明结果的样子,并为在真实环境中使用该算法提供指南。我们将在一个小型的社交网络图上进行演示,该图由少量节点以特定模式连接。示例图如下所示:
CREATE OR REPLACE TABLE EXAMPLE_DB.DATA_SCHEMA.PERSONS (NODEID VARCHAR, AGE NUMBER);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.PERSONS VALUES
('Dan', 18),
('Annie', 12),
('Matt', 22),
('Jeff', 51),
('Brie', 45),
('Elsa', 65),
('John', 64);
CREATE OR REPLACE TABLE EXAMPLE_DB.DATA_SCHEMA.KNOWS (SOURCENODEID VARCHAR, TARGETNODEID VARCHAR, WEIGHT FLOAT);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.KNOWS VALUES
('Dan', 'Annie', 1.0),
('Dan', 'Matt', 1.0),
('Annie', 'Matt', 1.0),
('Annie', 'Jeff', 1.0),
('Annie', 'Brie', 1.0),
('Matt', 'Brie', 3.5),
('Brie', 'Elsa', 1.0),
('Brie', 'Jeff', 2.0),
('John', 'Jeff', 1.0);
此图代表了七个相互认识的人。关系属性 weight 表示两人之间认识的强度。
运行作业
要运行查询,需要为应用程序、您的消费者角色和您的环境设置必要的权限。请参阅 入门 页面以了解更多信息。
我们还假设应用程序名称为默认的 Neo4j_Graph_Analytics。如果您在安装过程中选择了不同的应用程序名称,请将其替换为该名称。
CALL Neo4j_Graph_Analytics.graph.fast_rp('CPU_X64_XS', {
'defaultTablePrefix': 'EXAMPLE_DB.DATA_SCHEMA',
'project': {
'nodeTables': [ 'PERSONS' ],
'relationshipTables': {
'KNOWS': {
'sourceTable': 'PERSONS',
'targetTable': 'PERSONS',
'orientation': 'UNDIRECTED'
}
}
},
'compute': {
'resultProperty': 'embedding',
'featureProperties': ['AGE'],
'embeddingDimension': 4
},
'write': [{
'nodeLabel': 'PERSONS',
'outputTable': 'PERSONS_EMBEDDING',
'nodeProperty': 'embedding'
}]
});
| JOB_ID | JOB_STATUS | JOB_START | JOB_END | JOB_RESULT |
|---|---|---|---|---|
job_7204719cb7584b82829d6b3efaa14760 |
SUCCESS |
2025-07-01 08:56:06.352 |
2025-07-01 08:56:11.663 |
{
"fast_rp_1": {
"computeMillis": 58,
"configuration": {
"concurrency": 2,
"embeddingDimension": 4,
"featureProperties": ["AGE"],
"iterationWeights": [0, 1, 1],
"resultProperty": "embedding",
"nodeLabels": ["*"],
"nodeSelfInfluence": 0,
"normalizationStrength": 0,
"propertyRatio": 0,
"relationshipTypes": ["*"]
}
},
"project_1": {
"graphName": "snowgraph",
"nodeCount": 7,
"nodeLabels": ...,
"nodeMillis": 162,
"relationshipCount": 18,
"relationshipMillis": 361,
"relationshipTypes": ...,
"totalMillis": 523
},
"write_node_property_1": {
"nodeLabel": "PERSONS",
"nodeProperty": "embedding",
"outputTable": "EXAMPLE_DB.DATA_SCHEMA.PERSONS_EMBEDDING",
"rowsWritten": 7,
"writeMillis": 2086
}
} |
返回的结果包含有关作业执行和结果分布的信息。此外,每个节点的嵌入已回写到 Snowflake 数据库。我们可以这样查询:
SELECT * FROM EXAMPLE_DB.DATA_SCHEMA.PERSONS_EMBEDDING;
| NODEID | EMBEDDING |
|---|---|
Dan |
[1.3512902, 1.2954215, 0.53883743, 0.15730344] |
Annie |
[1.1745052, 1.3533449, 0.78872097, 0.3116792 ] |
Matt |
[1.1750009, 1.4353449, 0.62919945, 0.2507171 ] |
Jeff |
[0.85930216, 0.9969696, 1.2673354, 0.4413304 ] |
Brie |
[1.1701117, 1.2218789, 0.903811, 0.4010921 ] |
Elsa |
[1.0561231, 1.1750455, 0.97774005, 0.51353323] |
John |
[0.7519763, 0.87796444, 1.381917, 0.6259881 ] |
算法的结果并不直观,因为节点嵌入格式是节点在其邻域内的数学抽象,专为机器学习程序设计。我们可以看到嵌入有四个元素(按照 embeddingDimension 配置),并且数字相对较小(它们都在 [-2, 2] 的范围内)。数字的大小由 embeddingDimension、图中节点的数量以及 FastRP 对中间嵌入向量执行欧几里得归一化的事实控制。
|
由于算法的随机性质,不同运行之间的结果会有所不同。然而,这并不一定意味着两个节点嵌入之间的成对距离会有很大差异。 |
加权
下面是运行加权版算法的示例。
CALL Neo4j_Graph_Analytics.graph.fast_rp('CPU_X64_XS', {
'defaultTablePrefix': 'EXAMPLE_DB.DATA_SCHEMA',
'project': {
'nodeTables': [ 'PERSONS' ],
'relationshipTables': {
'KNOWS': {
'sourceTable': 'PERSONS',
'targetTable': 'PERSONS',
'orientation': 'UNDIRECTED'
}
}
},
'compute': {
'resultProperty': 'embedding',
'featureProperties': ['AGE'],
'embeddingDimension': 4,
'relationshipWeightProperty': 'WEIGHT'
},
'write': [{
'nodeLabel': 'PERSONS',
'outputTable': 'PERSONS_EMBEDDING',
'nodeProperty': 'embedding'
}]
});
| JOB_ID | JOB_STATUS | JOB_START | JOB_END | JOB_RESULT |
|---|---|---|---|---|
job_3778d8556d35458e839ccf3769c7a13f |
SUCCESS |
2025-07-01 09:45:35.514 |
2025-07-01 09:45:41.071 |
{
"fast_rp_1": {
"computeMillis": 31,
"configuration": {
"concurrency": 2,
"embeddingDimension": 4,
"featureProperties": ["AGE"],
"iterationWeights": [0, 1, 1],
"jobId": "d9d1ecb8-6e85-41e0-bfdd-453acc16170e",
"logProgress": true,
"resultProperty": "embedding",
"nodeLabels": ["*"],
"nodeSelfInfluence": 0,
"normalizationStrength": 0,
"propertyRatio": 0,
"relationshipTypes": ["*"],
"relationshipWeightProperty": "WEIGHT",
"sudo": false
},
"mutateMillis": 3,
"nodeCount": 7,
"nodePropertiesWritten": 7,
"preProcessingMillis": 11
},
"project_1": {
"graphName": "snowgraph",
"nodeCount": 7,
"nodeLabels": ...,
"nodeMillis": 165,
"relationshipCount": 18,
"relationshipMillis": 469,
"relationshipTypes": ...,
"totalMillis": 634
},
"write_node_property_1": {
"nodeLabel": "PERSONS",
"nodeProperty": "embedding",
"outputTable": "EXAMPLE_DB.DATA_SCHEMA.PERSONS_EMBEDDING",
"rowsWritten": 7,
"writeMillis": 2096
}
} |
SELECT * FROM EXAMPLE_DB.DATA_SCHEMA.PERSONS_EMBEDDING;
| NODEID | EMBEDDING |
|---|---|
Dan |
[1.6606944, 0.15601496, -0.022982463, -0.6949948 ] |
Annie |
[1.8600147, 0.05331285, -0.18106663, -0.703791 ] |
Matt |
[1.4891803, 0.16358498, -0.13624413, -0.7101983 ] |
Jeff |
[1.6981337, -0.06413156, -0.43987995, -0.48425722] |
Brie |
[1.5706161, 0.04065016, -0.19980223, -0.611043 ] |
Elsa |
[1.4356222, 0.13352287, -0.20066525, -0.6678886 ] |
John |
[1.5773826, -0.1315625, -0.823277, -0.4150287 ] |
由于算法的初始状态是随机的,因此无法直观地分析关系权重的影响。
使用节点属性作为特征
为了解释使用节点属性进行初始化的新颖之处,我们考虑一个示例,其中 embeddingDimension 为 10,propertyRatio 为 0.2。因此,嵌入属性的维度 propertyDimension 为 2。假设我们有一个标量类型的属性 f1,以及一个存储长度为 2 的数组的属性 f2。这意味着有 3 个特征,我们将它们按 f1 后跟 f2 的两个值的顺序排列。对于这三个特征中的每一个,我们采样一个二维随机向量。假设它们是 p1=[0.0, 2.4],p2=[-2.4, 0.0] 和 p3=[2.4, 0.0]。现在考虑一个节点 (n {f1: 0.5, f2: [1.0, -1.0]})。上述线性组合具体来说是 0.5 * p1 + 1.0 * p2 - 1.0 * p3 = [-4.8, 1.2]。节点 n 的初始随机向量包含前八个按照原始 FastRP 论文采样得到的值,然后是我们计算出的值 -4.8 和 1.2,总共 10 个条目。
在下面的示例中,我们再次将嵌入维度设为 2,但将 propertyRatio 设为 1,这意味着嵌入仅由节点属性计算得出。
CALL Neo4j_Graph_Analytics.graph.fast_rp('CPU_X64_XS', {
'defaultTablePrefix': 'EXAMPLE_DB.DATA_SCHEMA',
'project': {
'nodeTables': [ 'PERSONS' ],
'relationshipTables': {
'KNOWS': {
'sourceTable': 'PERSONS',
'targetTable': 'PERSONS',
'orientation': 'UNDIRECTED'
}
}
},
'compute': {
'resultProperty': 'embedding',
'embeddingDimension': 2,
'propertyRatio': 1.0,
'featureProperties': ['AGE'],
'iterationWeights': [1.0]
},
'write': [{
'nodeLabel': 'PERSONS',
'outputTable': 'PERSONS_EMBEDDING',
'nodeProperty': 'embedding'
}]
});
| JOB_ID | JOB_STATUS | JOB_START | JOB_END | JOB_RESULT |
|---|---|---|---|---|
job_0b4d5a36867b4927a9e43d485bc92978 |
SUCCESS |
2025-07-01 11:51:42.489 |
2025-07-01 11:51:48.125 |
{
"fast_rp_1": {
"computeMillis": 26,
"configuration": {
"concurrency": 2,
"embeddingDimension": 2,
"featureProperties": ["AGE"],
"iterationWeights": [1],
"jobId": "79df01bd-6f03-4e8c-927e-745c8dee5c5f",
"logProgress": true,
"resultProperty": "embedding",
"nodeLabels": ["*"],
"nodeSelfInfluence": 0,
"normalizationStrength": 0,
"propertyRatio": 1,
"relationshipTypes": ["*"],
"sudo": false
},
"mutateMillis": 2,
"nodeCount": 7,
"nodePropertiesWritten": 7,
"preProcessingMillis": 9
},
"project_1": {
"graphName": "snowgraph",
"nodeCount": 7,
"nodeLabels": ...,
"nodeMillis": 153,
"relationshipCount": 18,
"relationshipMillis": 546,
"relationshipTypes": ...,
"totalMillis": 699
},
"write_node_property_1": {
"nodeLabel": "PERSONS",
"nodeProperty": "embedding",
"outputTable": "EXAMPLE_DB.DATA_SCHEMA.PERSONS_EMBEDDING",
"rowsWritten": 7,
"writeMillis": 2118
}
} |
返回的结果包含有关作业执行和结果分布的信息。此外,每个节点的嵌入已回写到 Snowflake 数据库。我们可以这样查询:
SELECT * FROM EXAMPLE_DB.DATA_SCHEMA.PERSONS_EMBEDDING;
| NODEID | EMBEDDING |
|---|---|
Dan |
[-0.7071068, 0.7071068 ] |
Annie |
[-0.7071068, 0.7071068 ] |
Matt |
[-0.7071068, 0.7071068 ] |
Jeff |
[-0.70710677, 0.70710677] |
Brie |
[-0.70710677, 0.70710677] |
Elsa |
[-0.70710677, 0.70710677] |
John |
[-0.70710677, 0.70710677] |
在此示例中,嵌入基于 age 属性。由于对每次迭代应用了 L2 归一化(此处仅迭代一次),因此所有节点具有相同的嵌入,尽管它们的年龄值不同(舍入误差除外)。