md_files/科研/图神经网络.md

189 lines
5.8 KiB
Markdown
Raw Normal View History

2025-03-18 12:46:59 +08:00
## 图神经网络
图表示学习的本质是把节点映射成低维连续稠密的向量。这些向量通常被称为 **嵌入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;
}
```