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 算法所使用的语法。

运行 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 写入配置。
表 1. 参数
名称 类型 默认 可选 描述

computePoolSelector

字符串

不适用

用于运行 K-Means 作业的计算池选择器。

配置

Map

{}

用于图项目、算法计算和结果回写的配置。

配置映射由以下三个条目组成。

有关以下项目配置的更多详细信息,请参阅 项目文档
表 2. 项目配置
名称 类型

nodeTables

节点表列表。

relationshipTables

关系类型到关系表的映射。

表 3. 计算配置
名称 类型 默认 可选 描述

resultProperty

字符串

'community'

将回写到 Snowflake 数据库的节点属性。

nodeProperty

字符串

不适用

用于聚类的节点属性数组名称。必须是代表坐标或特征向量的数字数组属性。

k

整数

10

要创建的簇数量。必须大于 0 且小于或等于图中的节点数。

maxIterations

整数

10

运行的最大迭代次数。如果算法在达到此限制前收敛,将提前停止。

deltaThreshold

浮点数

0.05

收敛阈值(百分比)。如果簇分配的相对变化低于此值,算法将停止迭代。

numberOfRestarts

整数

1

使用不同随机初始化运行 K-Means 的次数。系统将保留畸变(distortion)最低的最佳结果。

randomSeed

整数

不适用

用于控制算法随机性的种子值。设置此值可确保多次运行结果的可重复性。

computeSilhouette

布尔值

false

是否计算轮廓系数 (silhouette score) 以评估簇质量。启用此项会增加计算开销。

seedCentroids

列表

不适用

可选的初始中心,作为坐标数组列表提供。提供此项时,k 必须与提供的中心数量匹配。

concurrency

整数

4

用于计算的并发线程数。

关于下文写入配置的更多详细信息,请参考 写入文档
表 4. 写入配置
名称 类型 默认 可选 描述

nodeLabel

字符串

不适用

内存图中将要回写其属性的节点标签。

nodeProperty

字符串

'community'

将要写入 Snowflake 数据库的节点属性。

outputTable

字符串

不适用

写入带有簇分配的节点属性的 Snowflake 数据库表。

K-Means 算法仅对节点属性进行操作,在聚类计算中不使用关系信息。不过,如果出于其他目的需要,关系仍可以作为图投影的一部分加载。

示例

在本节中,我们将展示在具体图上运行 K-Means 算法的示例。K-Means 对具有数字数组属性的节点进行操作,根据它们在这些属性定义的特征空间中的相似度进行聚类。

考虑以下包含代表纬度和经度的地理坐标的 City(城市)节点图。

Visualization of the example graph
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。如果您在安装过程中选择了不同的应用程序名称,请将其替换为该名称。

以下将运行 K-Means 作业
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'
    }]
});
表 5. 结果
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;
表 6. 结果
NODEID 社区版

伦敦

0

曼彻斯特

0

里士满

0

普罗夫迪夫

1

斯德哥尔摩

1

克里斯蒂安斯塔德

1

马尔默

1

结果显示,城市被分为两个簇。分配到簇 0 的城市往往位于英国,而簇 1 中的城市都位于东欧和斯堪的纳维亚半岛。这演示了 K-Means 如何根据坐标相似度有效地识别数据中的地理模式。

我们对大多数过程配置参数使用了默认值。randomSeed 参数设置为 26,以确保每次调用产生相同的结果以实现可重复性。k 参数设置为 2 以创建两个不同的簇,maxIterations 设置为 20 作为上限,尽管算法提前收敛了。

使用多次重启 (Restarts)

由于 K-Means 可能根据初始中心位置收敛到不同的局部最优解,因此使用不同的随机初始化多次运行它有助于找到更好的解决方案。numberOfRestarts 参数控制算法使用不同初始化运行的次数,并保留总畸变最小的结果。

以下将运行 K-Means,并进行 5 次不同的随机初始化
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'
    }]
});
表 7. 结果
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 是常见的做法。