md_files/科研/图神经网络.md
2025-03-18 12:46:59 +08:00

5.8 KiB
Raw Blame History

图神经网络

图表示学习的本质是把节点映射成低维连续稠密的向量。这些向量通常被称为 嵌入Embedding,它们能够捕捉节点在图中的结构信息和属性信息,从而用于下游任务(如节点分类、链接预测、图分类等)。

  • 低维:将高维的原始数据(如邻接矩阵或节点特征)压缩为低维向量,减少计算和存储开销。
  • 连续:将离散的节点或图结构映射为连续的向量空间,便于数学运算和捕捉相似性。
  • 稠密:将稀疏的原始数据转换为稠密的向量,每个维度都包含有意义的信息。

对图数据进行深度学习的“朴素做法”

把图的邻接矩阵和节点特征“直接拼接”成固定维度的输入,然后将其送入一个深度神经网络(全连接层)进行学习。

image-20250316142412685

这种做法面临重大问题,导致其并不可行

  1. O(|V|^2) 参数量 ,参数量庞大

  2. 无法适应不同大小的图 ,需要固定输入维度

  3. 对节点顺序敏感 ,节点编号顺序一变,输入就完全变样,但其实图的拓扑并没变(仅节点编号/排列方式不同)。

       A —— B
       |     |
       D —— C
    

    矩阵 1(顺序 $[A,B,C,D]$

    
    M_1 = 
    \begin{pmatrix}
     0 & 1 & 0 & 1\\
     1 & 0 & 1 & 0\\
     0 & 1 & 0 & 1\\
     1 & 0 & 1 & 0
    \end{pmatrix}.
    

    矩阵 2(顺序 $[C,A,D,B]$

    
    M_2 =
    \begin{pmatrix}
     0 & 0 & 1 & 1 \\
     0 & 0 & 1 & 1 \\
     1 & 1 & 0 & 0 \\
     1 & 1 & 0 & 0
    \end{pmatrix}.
    

两个矩阵完全不同,但它们对应的图是相同的(只不过节点的顺序改了)。

邻居聚合

计算图

图神经网络里,通常每个节点v 都有一个局部计算图,用来表示该节点在聚合信息时所需的所有邻居(及邻居的邻居……)的依赖关系。

  • 直观理解
    • 以节点 v 为根;
    • 1-hop 邻居在第一层2-hop 邻居在第二层……
    • 逐层展开直到一定深度(例如 k 层)。
    • 这样形成一棵“邻域树”或“展开图”,其中每个节点都需要从其子节点(邻居)获取特征进行聚合。

image-20250316152729679

image-20250316152836156

例子

在图神经网络中,每一层的计算通常包括以下步骤:

  1. 聚合Aggregation:将邻居节点的特征聚合起来(如求和、均值、最大值等)。

  2. 变换Transformation:将聚合后的特征通过一个神经网络(如 MLP进行非线性变换。

      A
      |
      B
    /   \
   C     D

假设每个节点的特征是一个二维向量:

  • 节点 A 的特征:h_A = [1.0, 0.5]
  • 节点 B 的特征:h_B = [0.8, 1.2]
  • 节点 C 的特征:h_C = [0.3, 0.7]
  • 节点 D 的特征:h_D = [1.5, 0.9]

第 1 层更新:$A^{(0)} \to A^{(1)}$

  1. 节点 A 的 1-hop 邻居:只有 $B$。

  2. 聚合(示例:自 + sum 邻居):

    
    z_A^{(1)} \;=\; A^{(0)} + B^{(0)}
               \;=\; [1.0,\,0.5] + [0.8,\,1.2]
               \;=\; [1.8,\,1.7].
    
  3. MLP 变换用一个MLP映射 z_A^{(1)} 到 2 维输出:

    
    A^{(1)} \;=\; \mathrm{MLP_1}\bigl(z_A^{(1)}\bigr).
    
    • (数值略,可想象 \mathrm{MLP}([1.8,1.7]) \approx [1.9,1.1] 之类。)

结果A^{(1)} 包含了 A 的初始特征 + B 的初始特征信息。


第 2 层更新:$A^{(1)} \to A^{(2)}$

为了让 A 获得 2-hop 范围($C, D$)的信息,需要B 在第 1 层就吸收了 C, D 的特征,从而 B^{(1)} 蕴含 C, D 信息。然后 A 在第 2 层再从 B^{(1)} 聚合。

  1. 节点 B 在第 1 层(简要说明)

    • 邻居:\{A,C,D\}
    • 聚合:z_B^{(1)} = B^{(0)} + A^{(0)} + C^{(0)} + D^{(0)}
    • MLP 变换:$B^{(1)} = \mathrm{MLP}\bigl(z_B^{(1)}\bigr)$。
    • 此时 B^{(1)} 已经包含了 C, D 的信息。
  2. 节点 A 的第 2 层聚合

    • 邻居:$B$,但此时要用 B^{(1)}(它已吸收 C、D

    • 聚合

      
      z_A^{(2)} = A^{(1)} + B^{(1)}.
      
    • MLP 变换

      
      A^{(2)} = \mathrm{MLP_2}\bigl(z_A^{(2)}\bigr).
      

结果A^{(2)} 就包含了 2-hop 范围的信息,因为 B^{(1)} 中有 C, D 的贡献。

GNN 的层数就是节点聚合邻居信息的迭代次数,对应了“节点感受 k-hop 邻域”的深度。每层中,节点会用上一层的邻居表示进行聚合并经过可学习的变换,最终得到本层新的节点表示。

同一层里,所有节点共享一组参数(同一个 MLP 或线性变换)

public boolean hasCycle(ListNode head) {
        // 如果链表为空或者只有一个节点,直接返回无环
        if (head == null || head.next == null) {
            return false;
        }

        // 用 originalHead 保存最初的头节点
        ListNode originalHead = head;
        // 从 head.next 开始遍历,先把 head 与链表分离
        ListNode cur = head.next;
        ListNode pre = head;
        // 断开 head 和后面的连接
        head.next = null;

        while (cur != null) {
            // 如果当前节点又指回了 originalHead则说明出现环
            if (cur == originalHead) {
                return true;
            }
            // 反转指针
            ListNode temp = cur.next;
            cur.next = pre;
            // 移动 pre 和 cur
            pre = cur;
            cur = temp;
        }

        // 走到空指针,说明无环
        return false;
    }