189 lines
5.8 KiB
Markdown
189 lines
5.8 KiB
Markdown
|
## 图神经网络
|
|||
|
|
|||
|
图表示学习的本质是把节点映射成低维连续稠密的向量。这些向量通常被称为 **嵌入(Embedding)**,它们能够捕捉节点在图中的结构信息和属性信息,从而用于下游任务(如节点分类、链接预测、图分类等)。
|
|||
|
|
|||
|
- **低维**:将高维的原始数据(如邻接矩阵或节点特征)压缩为低维向量,减少计算和存储开销。
|
|||
|
- **连续**:将离散的节点或图结构映射为连续的向量空间,便于数学运算和捕捉相似性。
|
|||
|
- **稠密**:将稀疏的原始数据转换为稠密的向量,每个维度都包含有意义的信息。
|
|||
|
|
|||
|
|
|||
|
|
|||
|
### 对图数据进行深度学习的“朴素做法”
|
|||
|
|
|||
|
把图的邻接矩阵和节点特征“直接拼接”成固定维度的输入,然后将其送入一个深度神经网络(全连接层)进行学习。
|
|||
|
|
|||
|

|
|||
|
|
|||
|
这种做法面临重大问题,导致其**并不可行**:
|
|||
|
|
|||
|
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 层)。
|
|||
|
- 这样形成一棵“邻域树”或“展开图”,其中每个节点都需要从其子节点(邻居)获取特征进行聚合。
|
|||
|
|
|||
|
|
|||
|
|
|||
|

|
|||
|
|
|||
|

|
|||
|
|
|||
|
**例子**
|
|||
|
|
|||
|
在图神经网络中,每一层的计算通常包括以下步骤:
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
```
|
|||
|
|