## 图神经网络 图表示学习的本质是把节点映射成低维连续稠密的向量。这些向量通常被称为 **嵌入(Embedding)**,它们能够捕捉节点在图中的结构信息和属性信息,从而用于下游任务(如节点分类、链接预测、图分类等)。 - **低维**:将高维的原始数据(如邻接矩阵或节点特征)压缩为低维向量,减少计算和存储开销。 - **连续**:将离散的节点或图结构映射为连续的向量空间,便于数学运算和捕捉相似性。 - **稠密**:将稀疏的原始数据转换为稠密的向量,每个维度都包含有意义的信息。 ### 对图数据进行深度学习的“朴素做法” 把图的邻接矩阵和节点特征“直接拼接”成固定维度的输入,然后将其送入一个深度神经网络(全连接层)进行学习。 ![image-20250316142412685](D:\folder\test\output\image-20250316142412685.png) 这种做法面临重大问题,导致其**并不可行**: 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](D:\folder\test\output\image-20250316152729679.png) ![image-20250316152836156](D:\folder\test\output\image-20250316152836156.png) **例子** 在图神经网络中,每一层的计算通常包括以下步骤: 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; } ```