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;
|
||
}
|
||
```
|
||
|