跳转至
开发版采用单机部署,无需产品 License 文件。需要多机部署或大规模性能测试? 了解企业版

标签传播算法(LPA)

本文介绍标签传播算法(Label Propagation Algorithm, LPA),并演示如何用 GQL 实现该算法。

算法介绍

标签传播算法(LPA)是一种用于图中社区检测的算法。它通过基于邻居点的标签在网络中传播标签并形成社区。算法开始时,每个点被分配一个唯一的标签,并根据其邻居中最频繁的标签迭代更新点的标签。

该算法的工作原理是标签通常会在高度连接的点群体内部传播和保留,而难以跨越稀疏连接的区域。随着标签的传播,只有少数标签会保留下来,而大多数标签会消失,直到收敛。最终,具有相同标签的点被分组为一个社区。

标签传播算法的关键步骤如下:

  1. 开始时,每个点具有一个唯一的标签。
  2. 标签在网络中传播。
  3. 在每次迭代中,点将其标签更新为其邻居中最频繁的标签。
  4. 该过程重复进行,直到没有标签变化或达到所设定最大迭代次数。
  5. 最终输出每个点的社区标签,表示图中检测到的社区。

有关标签传播算法的更多信息,请参见标签传播算法。

数据模型

为了用 GQL 实现标签传播算法,将基于以下数据模型来创建图类型:

  • 人员表示为类型为Person的点。每个点分配有一个由唯一的id表示的初始标签。
  • 人员之间的关系编码为类型为Knows的边。这些边将点连接起来并作为标签传播的通道,使标签能够在网络中扩散。

有关图类型的完整定义,请参见准备图数据。

过程实现

为了基于邻居中最频繁的标签找到点的社区标签,创建了一个过程来实现标签传播算法。

该过程将Knows边视为无向边,并使用活动集变量跟踪标签发生变化的点。每轮只对这些点的邻居重新计算标签,减少图逐渐收敛时的边扫描。重新计算时仍统计待处理点的全部邻居的标签,而不只是标签发生变化的邻居;标签选择和更新规则与全量扫描版本一致。

有关过程的完整定义,请参见创建过程。以下部分描述了该过程的关键要素及其工作原理。

该过程接收以下输入参数:

  • max_iter:最大迭代次数。

此外,在过程内部定义了以下变量:

变量 类型 说明
node_label 点绑定 SumAgg 聚合值变量 每个点的标签。
node_count 点绑定 SumAgg 聚合值变量 上次更新点标签时,该标签在邻居中的出现次数。初始值为0,用于比较是否更新标签。
label_count_map 点绑定 MapAgg 聚合值变量 用于统计点邻居中每个标签出现次数的映射。
changed 点绑定 OrAgg 聚合值变量 点的标签是否在本轮发生变化。每次重新计算该点后覆盖此标记。
changed_set 活动集变量 上一轮标签发生变化的点集合。初始包含所有点,之后每轮替换为本轮标签发生变化的点。
recompute_set 活动集变量 本轮需要重新计算标签的点集合,即changed_set中点的邻居。
result_file 文件变量 保存最终点标签的文件,包括点 ID 及其最终社区标签。
new_active 全局 OrAgg 聚合值变量 全局布尔标记,表示本轮是否发生更新;决定是否继续迭代。
iter 原始值变量 算法执行后迭代的次数。
cur_iter 原始值变量 遍历label_count_list时使用的索引。
label_count_list 原始值变量(记录列表) 读取label_count_map得到的标签计数列表,每条记录的_0为标签、_1为出现次数。
did_change 原始值变量(布尔值) 本次重新计算是否更新了点的标签,用于覆盖changed。

以下步骤概述了该过程的逻辑:

  1. 初始化每个点的node_label为其自身 ID,并将所有点加入changed_set,以启动第一轮计算。
  2. 当new_active为true且当前迭代次数小于最大迭代次数(由max_iter指定)时重复执行:
    1. 将new_active设为false,并增加迭代计数器。
    2. 确定待重算点:从changed_set中的点出发,将其邻居收集到recompute_set,覆盖上一轮的集合。
    3. 重新计算标签:对于recompute_set中的每个点t,将其全部邻居s的标签聚合到t的label_count_map中。从label_count_list选出出现次数最多的标签;次数相同时选择值最大的标签。仅当候选标签与当前标签不同且其计数大于node_count,或计数等于node_count且候选标签值更大时,更新点的标签和node_count,并将new_active设为true。用did_change覆盖该点的changed标记,然后清空标签计数映射。
    4. 更新变化点集合:从recompute_set中筛选changed为true的点,用这些点替换changed_set,供下一轮使用。
  3. 收敛后或达到最大迭代次数后,将每个点的 ID 和最终对应的社区标签 ID 导出到result_file。

没有邻居的孤立点不会进入recompute_set,其最终标签保留为自身 ID。

操作步骤

在悦数图管理的控制台中,按顺序执行以下代码块。首次使用时,先按准备算法示例数据确认操作位置和目录挂载。

1. 准备图数据

  1. 创建与数据模型匹配的图类型:

    '''
    CREATE GRAPH TYPE IF NOT EXISTS lpa_graph_type AS {
        NODE Person (LABEL Person {id INT PRIMARY KEY}),
        EDGE Knows (Person)-[LABEL Knows {MULTIEDGE KEY ()}]->(Person)
        }
    '''
    
  2. 下载以下 CSV 文件,保留文件名,保存到部署目录下的examples/algorithms/lpa/:

    查看 CSV 内容
    lpa_node.csv
    id
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    lpa_edge.csv
    src,dst
    0,1
    0,2
    0,3
    0,4
    0,5
    0,6
    0,7
    0,8
    0,10
    0,11
    0,12
    0,13
    0,17
    0,19
    0,21
    0,31
    1,2
    1,3
    1,7
    1,13
    1,17
    1,19
    1,21
    1,30
    2,3
    2,7
    2,8
    2,9
    2,13
    2,27
    2,28
    2,32
    3,7
    3,12
    3,13
    4,6
    4,10
    5,6
    5,10
    5,16
    6,16
    8,30
    8,32
    8,33
    9,33
    13,33
    14,32
    14,33
    15,32
    15,33
    18,32
    18,33
    19,33
    20,32
    20,33
    22,32
    22,33
    23,25
    23,27
    23,29
    23,32
    23,33
    24,25
    24,27
    24,31
    25,31
    26,29
    26,33
    27,33
    28,31
    28,33
    29,32
    29,33
    30,32
    30,33
    31,32
    31,33
    32,33
    
  3. 基于图类型创建一个临时图,并将数据文件加载到临时图中。

    CREATE TEMPORARY GRAPH IF NOT EXISTS #lpa_graph TYPED lpa_graph_type
    
    '''
    USE #lpa_graph IMPORT INTO GRAPH {
        NODE (v@Person {id: id}) FROM DATAFILE {
            PATH: "file:///examples/algorithms/lpa/lpa_node.csv",
            FORMAT: "CSV",
            DELIMITER: ','
        },
        EDGE (id: src)-[e@Knows{}]->(id: dst) FROM DATAFILE {
            PATH: "file:///examples/algorithms/lpa/lpa_edge.csv",
            FORMAT: "CSV",
            DELIMITER: ','
        }
        }
    '''
    

    导入成功后,在控制台执行以下查询核对点数:

    '''
    USE #lpa_graph
    MATCH (n)
    RETURN count(n) AS node_count
    '''
    

    node_count应为34。点数不符时,先按文件问题排查检查文件和导入结果。

2. 创建过程

创建实现标签传播算法的过程。关于该过程的更多信息,请参见过程实现。

'''
CREATE OR REPLACE PROCEDURE lpa (max_iter INT) RETURNS () AS {
        NODE VALUE node_label      SumAgg<INT> = 0
        NODE VALUE node_count      SumAgg<INT> = 0
        NODE VALUE label_count_map MapAgg<INT, SumAgg<INT>>
        NODE VALUE changed         OrAgg = false
        FILE       result_file     {id INT, label_id INT}= DATAFILE {
                                    PATH: "file:///examples/algorithms/lpa/output",
                                    FORMAT: "CSV"
                                    }
        VALUE      new_active      OrAgg = true
        VALUE      iter = 0
        VALUE      changed_set     ACTIVE_SET
        VALUE      recompute_set   ACTIVE_SET

        MATCH (a@Person)
        PER NODE (a) {
          SET a.@node_label = a.id
        }
        FINALLY {
          SET changed_set = a
        }

        WHILE @new_active AND iter < max_iter THEN {
          SET @new_active = false
          SET iter = iter + 1

          // P1:收集上一轮标签发生变化的点的邻居
          MATCH (c@Person)-[@Knows]-(t@Person)
          WHERE c IN changed_set
          FINALLY {
            SET recompute_set = t
          }

          // P2:统计 recompute_set 中各点的全部邻居标签
          MATCH (t@Person)-[@Knows]-(s@Person)
          WHERE t IN recompute_set
          PER PATH {
            SET t.@label_count_map += TUPLE(s.@node_label, 1)
          }
          PER NODE (t) {
            VALUE max_count INT = 0
            VALUE max_label INT = -1
            VALUE cur_iter = 0
            VALUE did_change = false
            VALUE label_count_list = t.@label_count_map
            WHILE cur_iter < length(label_count_list) THEN {
              VALUE val = label_count_list[cur_iter]
              IF val._1 > max_count OR val._1 = max_count AND val._0 > max_label THEN {
                SET max_count = val._1
                SET max_label = val._0
              }
              SET cur_iter = cur_iter + 1
            }
            IF max_label <> -1 AND (max_count > t.@node_count AND max_label <> t.@node_label
             OR max_count = t.@node_count AND max_label > t.@node_label
            ) THEN {
              SET @new_active += true
              SET t.@node_count = max_count
              SET t.@node_label = max_label
              SET did_change = true
            }
            SET t.@changed = did_change
            SET t.@label_count_map.clear()
          }

          // P3:仅保留本轮标签发生变化的点
          MATCH (t@Person)
          WHERE t IN recompute_set AND t.@changed = true
          FINALLY {
            SET changed_set = t
          }
        }

        MATCH (a@Person)
        PER NODE (a) {
          EXPORT a.id, a.@node_label INTO result_file
        }
      }
'''

3. 调用过程

调用该过程计算每个点最终的社区标签。以下示例将最大迭代次数设置为10。

USE #lpa_graph CALL lpa(10) FINISH

此调用将结果写入 CSV,控制台不返回结果表。

在宿主机的部署目录下,打开examples/algorithms/lpa/output/中的 CSV 文件。示例内容如下:

"id","label_id"
0,33
1,33
2,33
3,33
4,31
5,31
6,31
7,33
8,33
9,33
10,31
11,33
12,33
13,33
14,33
15,33
16,31
17,33
18,33
19,33
20,33
21,33
22,33
23,33
24,33
25,33
26,33
27,33
28,33
29,33
30,33
31,33
32,33
33,33

id代表点的初始标签,label_id代表点最终的社区标签。

将结果写入图中

如果希望将 LPA 结果持久化到图,请参见 EXPORT 并按 LPA 输出做字段映射:

  • 先将 LPA 输出字段导出到表变量(通常为id、label_id)。
  • 若写入远程图,使用table_split(...)分批,再通过nebula_exec批量执行USE + INSERT。

建议在目标图类型中增加社区字段,例如在Person上增加community_id属性。