K-均值聚类 (K-Means Clustering)
简介
K-Means 聚类算法用于根据节点的属性将节点划分为不同的簇。该算法将节点进行分组,使得同一簇内的节点具有相似的属性值,而不同簇之间的节点则不相似。
K-Means 的工作原理是迭代地将节点分配给最近的簇中心,然后根据当前的簇分配情况重新计算簇中心。该算法需要预先指定簇的数量 k,并适用于数字数组属性,例如坐标、嵌入或特征向量。
算法会执行以下步骤,直到收敛或达到最大迭代次数。首先,它会随机或根据提供的种子中心初始化 k 个簇中心。然后,根据欧几里得距离将每个节点分配给最近的簇中心。接下来,它会将每个中心重新计算为分配给该簇的所有节点的平均值。算法会重复执行分配和更新步骤,直到簇分配稳定或达到最大迭代次数。
迭代次数由配置参数 maxIterations 限制。如果簇分配的变化小于 deltaThreshold 参数指定的阈值,算法可能会提前停止。
K-Means 特别适用于客户细分、地理位置聚类、在特征空间中对相似实体进行分组,以及作为其他机器学习任务的预处理步骤。该算法在大数据集上具有良好的扩展性,并通过簇中心提供可解释的结果。
有关此算法的更多信息,请参阅
算法注意事项
K-Means 算法有几个重要的特性需要考虑。簇的数量 k 必须预先指定,这通常需要领域知识或肘部法则 (elbow method) 等经验方法来确定。由于算法对初始化敏感,结果可能会因初始中心位置的不同而有所差异。通过 numberOfRestarts 参数使用不同的随机种子多次运行算法,有助于找到更好的解决方案。
该算法使用欧几里得距离作为度量标准,这假设所有维度同等重要且处于可比较的量级。当维度具有不同的范围或单位时,可能需要进行特征归一化或标准化。K-Means 并不总能收敛到全局最优解,而是找到局部最优解,不同初始化运行的结果可能存在差异。
该算法最适合处理大小相似的球形簇,对于细长或形状不规则的簇可能效果不佳。为了评估簇的质量,可以启用可选的 computeSilhouette 参数,但这会增加计算开销,因为它需要计算所有节点对之间的距离。
语法
本节介绍执行 K-Means 算法所使用的语法。
CALL Neo4j_Graph_Analytics.graph.kmeans(
'CPU_X64_XS', (1)
{
['defaultTablePrefix': '...',] (2)
'project': {...}, (3)
'compute': {...}, (4)
'write': {...} (5)
}
);
| 1 | 计算池选择器。 |
| 2 | 表引用的可选前缀。 |
| 3 | 项目配置。 |
| 4 | 计算配置。 |
| 5 | 写入配置。 |
| 名称 | 类型 | 默认 | 可选 | 描述 |
|---|---|---|---|---|
computePoolSelector |
字符串 |
|
否 |
用于运行 K-Means 作业的计算池选择器。 |
配置 |
Map |
|
否 |
用于图项目、算法计算和结果回写的配置。 |
配置映射由以下三个条目组成。
| 有关以下项目配置的更多详细信息,请参阅 项目文档。 |
| 名称 | 类型 |
|---|---|
nodeTables |
节点表列表。 |
relationshipTables |
关系类型到关系表的映射。 |
| 名称 | 类型 | 默认 | 可选 | 描述 |
|---|---|---|---|---|
resultProperty |
字符串 |
|
是 |
将回写到 Snowflake 数据库的节点属性。 |
nodeProperty |
字符串 |
|
否 |
用于聚类的节点属性数组名称。必须是代表坐标或特征向量的数字数组属性。 |
k |
整数 |
|
是 |
要创建的簇数量。必须大于 0 且小于或等于图中的节点数。 |
maxIterations |
整数 |
|
是 |
运行的最大迭代次数。如果算法在达到此限制前收敛,将提前停止。 |
deltaThreshold |
浮点数 |
|
是 |
收敛阈值(百分比)。如果簇分配的相对变化低于此值,算法将停止迭代。 |
numberOfRestarts |
整数 |
|
是 |
使用不同随机初始化运行 K-Means 的次数。系统将保留畸变(distortion)最低的最佳结果。 |
randomSeed |
整数 |
|
是 |
用于控制算法随机性的种子值。设置此值可确保多次运行结果的可重复性。 |
computeSilhouette |
布尔值 |
|
是 |
是否计算轮廓系数 (silhouette score) 以评估簇质量。启用此项会增加计算开销。 |
seedCentroids |
列表 |
|
是 |
可选的初始中心,作为坐标数组列表提供。提供此项时,k 必须与提供的中心数量匹配。 |
concurrency |
整数 |
|
是 |
用于计算的并发线程数。 |
| 关于下文写入配置的更多详细信息,请参考 写入文档。 |
| 名称 | 类型 | 默认 | 可选 | 描述 |
|---|---|---|---|---|
nodeLabel |
字符串 |
|
否 |
内存图中将要回写其属性的节点标签。 |
nodeProperty |
字符串 |
|
是 |
将要写入 Snowflake 数据库的节点属性。 |
outputTable |
字符串 |
|
否 |
写入带有簇分配的节点属性的 Snowflake 数据库表。 |
|
K-Means 算法仅对节点属性进行操作,在聚类计算中不使用关系信息。不过,如果出于其他目的需要,关系仍可以作为图投影的一部分加载。 |
示例
在本节中,我们将展示在具体图上运行 K-Means 算法的示例。K-Means 对具有数字数组属性的节点进行操作,根据它们在这些属性定义的特征空间中的相似度进行聚类。
考虑以下包含代表纬度和经度的地理坐标的 City(城市)节点图。
CREATE OR REPLACE TABLE EXAMPLE_DB.DATA_SCHEMA.NODES_CITY (NODEID VARCHAR, COORDINATES ARRAY);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'London', ARRAY_CONSTRUCT(51.39148, -0.29825);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'Manchester', ARRAY_CONSTRUCT(53.41058, -2.97794);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'Richmond', ARRAY_CONSTRUCT(51.41259, -0.2974);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'Plovdiv', ARRAY_CONSTRUCT(42.68583, 26.32917);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'Stockholm', ARRAY_CONSTRUCT(59.36004, 18.00086);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'Kristianstad', ARRAY_CONSTRUCT(56.28338, 13.27773);
INSERT INTO EXAMPLE_DB.DATA_SCHEMA.NODES_CITY SELECT 'Malmo', ARRAY_CONSTRUCT(55.60587, 13.00073);
在此示例中,我们希望使用 K-Means 算法根据地理坐标对城市进行分组。我们将把城市聚类为 2 组,以识别地理位置接近的城市。
利用 Snowflake 中的节点表,我们现在可以将其作为算法作业的一部分进行投影。在接下来的示例中,我们将演示如何在图上使用 K-Means 算法。
运行作业
运行 K-Means 作业包括投影 (Project)、计算 (Compute) 和写入 (Write) 三个步骤。
要运行查询,需要为应用程序、您的消费者角色和您的环境设置必要的权限。请参阅 入门 页面以了解更多信息。
我们还假设应用程序名称为默认的 Neo4j_Graph_Analytics。如果您在安装过程中选择了不同的应用程序名称,请将其替换为该名称。
CALL Neo4j_Graph_Analytics.graph.kmeans('CPU_X64_XS', {
'defaultTablePrefix': 'EXAMPLE_DB.DATA_SCHEMA',
'project': {
'nodeTables': ['NODES_CITY'],
'relationshipTables': {}
},
'compute': {
'nodeProperty': 'COORDINATES',
'k': 2,
'randomSeed': 26,
'maxIterations': 20,
},
'write': [{
'nodeLabel': 'nodes_city',
'outputTable': 'CITY_CLUSTERS'
}]
});
| JOB_ID | JOB_STATUS | JOB_START | JOB_END | JOB_RESULT |
|---|---|---|---|---|
job_kmeans_abc123def456 |
SUCCESS |
2025-10-22 15:30:45.123000 |
2025-10-22 15:30:52.456000 |
{
"kmeans_1": {
"averageDistanceToCentroid": 4.930111539422151,
"averageSilhouette": 0,
"centroids": [
[
52.07155,
-1.1911966666666667
],
[
53.48378,
17.6521225
]
],
"communityDistribution": {
"max": 4,
"mean": 3.5,
"min": 3,
"p1": 3,
"p10": 3,
"p25": 3,
"p5": 3,
"p50": 3,
"p75": 4,
"p90": 4,
"p95": 4,
"p99": 4,
"p999": 4
},
"computeMillis": 19,
"configuration": {
"computeSilhouette": false,
"concurrency": 6,
"deltaThreshold": 0.001,
"initialSampler": "UNIFORM",
"k": 2,
"maxIterations": 20,
"nodeLabels": [
"*"
],
"nodeProperty": "COORDINATES",
"numberOfRestarts": 1,
"randomSeed": 26,
"relationshipTypes": [
"*"
],
"resultProperty": "community",
"seedCentroids": []
},
"nodePropertiesWritten": 7
},
"project_1": {
"graphName": "snowgraph",
"nodeCount": 7,
"nodeLabels": {
"NODES_CITY": {
"count": 7,
"nodeId": {
"dataType": "STRING"
},
"properties": {
"COORDINATES": {
"dataType": "DOUBLE_ARRAY"
}
},
"table": "EXAMPLE_DB.DATA_SCHEMA.NODES_CITY"
}
},
"nodeMillis": 138,
"relationshipCount": 0,
"relationshipMillis": 0,
"relationshipTypes": {},
"totalMillis": 138
},
"write_node_property_1": {
"copyIntoTableMillis": 955,
"nodeLabel": "NODES_CITY",
"nodeProperty": "community",
"outputTable": "EXAMPLE_DB.DATA_SCHEMA.OUTPUT",
"rowsWritten": 7,
"stageUploadMillis": 581,
"writeMillis": 1743
}
} |
返回的结果包含有关作业执行和聚类结果的信息。ranIterations 字段显示算法在收敛前运行了 3 次迭代,didConverge 字段为 true 也证明了这一点。communityDistribution 提供了关于每个簇大小的统计信息,显示一个簇有 3 个城市,另一个有 4 个城市。
centroids 字段显示了最终的簇中心坐标。第一个坐标 [51.022935, 22.165015] 的中心代表一个簇,而第二个坐标 [53.62078, 4.540974] 的中心代表另一个。这些中心可以被解释为每个簇中城市的平均地理位置。
每个城市的簇分配结果已写回 Snowflake 数据库。我们可以查询结果来查看哪些城市属于哪个簇。
SELECT * FROM EXAMPLE_DB.DATA_SCHEMA.CITY_CLUSTERS ORDER BY COMMUNITY;
| NODEID | 社区版 |
|---|---|
伦敦 |
0 |
曼彻斯特 |
0 |
里士满 |
0 |
普罗夫迪夫 |
1 |
斯德哥尔摩 |
1 |
克里斯蒂安斯塔德 |
1 |
马尔默 |
1 |
结果显示,城市被分为两个簇。分配到簇 0 的城市往往位于英国,而簇 1 中的城市都位于东欧和斯堪的纳维亚半岛。这演示了 K-Means 如何根据坐标相似度有效地识别数据中的地理模式。
我们对大多数过程配置参数使用了默认值。randomSeed 参数设置为 26,以确保每次调用产生相同的结果以实现可重复性。k 参数设置为 2 以创建两个不同的簇,maxIterations 设置为 20 作为上限,尽管算法提前收敛了。
使用多次重启 (Restarts)
由于 K-Means 可能根据初始中心位置收敛到不同的局部最优解,因此使用不同的随机初始化多次运行它有助于找到更好的解决方案。numberOfRestarts 参数控制算法使用不同初始化运行的次数,并保留总畸变最小的结果。
CALL Neo4j_Graph_Analytics.graph.kmeans('CPU_X64_XS', {
'defaultTablePrefix': 'EXAMPLE_DB.DATA_SCHEMA',
'project': {
'nodeTables': ['NODES_CITY'],
'relationshipTables': {}
},
'compute': {
'nodeProperty': 'COORDINATES',
'k': 2,
'maxIterations': 20,
'numberOfRestarts': 5
},
'write': [{
'nodeLabel': 'nodes_city',
'outputTable': 'CITY_CLUSTERS_RESTARTS'
}]
});
| JOB_ID | JOB_STATUS | JOB_START | JOB_END | JOB_RESULT |
|---|---|---|---|---|
job_kmeans_restarts_def789 |
SUCCESS |
2025-10-22 15:45:22.123000 |
2025-10-22 15:45:30.456000 |
{
"kmeans_1": {
"averageDistanceToCentroid": 4.930111539422151,
"averageSilhouette": 0,
"centroids": [
[
52.07155,
-1.1911966666666667
],
[
53.48378,
17.6521225
]
],
"communityDistribution": {
"max": 4,
"mean": 3.5,
"min": 3,
"p1": 3,
"p10": 3,
"p25": 3,
"p5": 3,
"p50": 3,
"p75": 4,
"p90": 4,
"p95": 4,
"p99": 4,
"p999": 4
},
"computeMillis": 23,
"configuration": {
"computeSilhouette": false,
"concurrency": 6,
"deltaThreshold": 0.001,
"initialSampler": "UNIFORM",
"k": 2,
"maxIterations": 20,
"nodeLabels": [
"*"
],
"nodeProperty": "COORDINATES",
"numberOfRestarts": 5,
"randomSeed": 26,
"relationshipTypes": [
"*"
],
"resultProperty": "community",
"seedCentroids": []
},
"nodePropertiesWritten": 7
},
"project_1": {
"graphName": "snowgraph",
"nodeCount": 7,
"nodeLabels": {
"NODES_CITY": {
"count": 7,
"nodeId": {
"dataType": "STRING"
},
"properties": {
"COORDINATES": {
"dataType": "DOUBLE_ARRAY"
}
},
"table": "KRAKEN_DB.GDS.NODES_CITY"
}
},
"nodeMillis": 179,
"relationshipCount": 0,
"relationshipMillis": 0,
"relationshipTypes": {},
"totalMillis": 179
},
"write_node_property_1": {
"copyIntoTableMillis": 1254,
"nodeLabel": "NODES_CITY",
"nodeProperty": "community",
"outputTable": "KRAKEN_DB.GDS.OUTPUT",
"rowsWritten": 7,
"stageUploadMillis": 515,
"writeMillis": 2047
}
} |
算法使用不同的初始化运行了 5 次,并保留了所有节点中总畸变最小的最佳结果。computeMillis 值为 23 毫秒,代表执行 K-Means 算法本身所花费的时间,而整个作业执行耗时 202 毫秒(179 毫秒用于投影 + 23 毫秒用于计算)。这证明了运行多次重启并不一定会显著增加计算时间,因为重启在算法内部得到了有效处理。所有重启中产生的最佳聚类结果将写入输出表。
使用更多重启会增加计算时间,但可以通过避免陷入较差的局部最优解来提高聚类质量。当您没有领域知识来提供良好的初始中心,或者数据具有复杂的结构导致难以通过单次运行找到理想簇时,这一点尤其有用。对于簇质量至关重要的生产用例,将 numberOfRestarts 设置为 5 或 10 是常见的做法。