PyTorch深度学习框架60天进阶学习计划-第31天:图神经网络基础
目录
引言:图数据和图神经网络
欢迎来到PyTorch深度学习框架60天进阶学习计划的第31天!今天我们将深入学习图神经网络(Graph Neural Networks, GNN)的基础知识。
在我们的日常生活和科研中,许多数据天然地以图结构存在:社交网络中的人与关系、分子中的原子与化学键、知识图谱中的实体与关联、交通网络中的路口与道路等。传统的深度学习模型如CNN和RNN主要处理欧几里得空间的数据(如图像、序列),但对于这些非欧几里得空间的图数据,它们往往力不从心。
图神经网络应运而生,它能够同时捕捉节点特征和拓扑结构信息,为图数据分析提供了强大工具。今天,我们将重点解析图卷积网络的消息传递机制,对比谱域与空域方法的计算差异,并推导节点嵌入的聚合公式。
让我们开始这段精彩的图神经网络之旅吧!
图的基本概念与表示
在深入GNN之前,先来复习一下图的基本概念。
图的定义和表示
图 G = ( V , E ) G = (V, E) G=(V,E) 由节点集合 V V V 和边集合 E E E 组成。其中:
- V = { v 1 , v 2 , . . . , v N } V = \{v_1, v_2, ..., v_N\} V={v1,v2,...,vN} 表示 N N N 个节点
- E ⊆ V × V E \subseteq V \times V E⊆V×V 表示节点间的边
在计算机中,图通常通过以下几种方式表示:
1. 邻接矩阵
邻接矩阵 A ∈ R N × N A \in \mathbb{R}^{N \times N} A∈RN×N 是一个方阵,其中:
- A i j = 1 A_{ij} = 1 Aij=1 如果节点 i i i 和节点 j j j 之间有边
- A i j = 0 A_{ij} = 0 Aij=0 如果节点 i i i 和节点 j j j 之间没有边
- 对于带权图, A i j A_{ij} Aij 可以是边的权重
2. 邻接表
邻接表是一种更节省空间的表示方法,特别是对于稀疏图。对每个节点,我们存储与其相连的所有节点的列表。
3. 特征矩阵
节点特征矩阵 X ∈ R N × F X \in \mathbb{R}^{N \times F} X∈RN×F,其中 N N N 是节点数, F F F 是特征维度。每个节点 v i v_i vi 对应一个特征向量 x i ∈ R F x_i \in \mathbb{R}^F xi∈RF。
图的类型
图可以分为多种类型:
- 无向图/有向图:边是否有方向
- 带权图/无权图:边是否有权重
- 同构图/异构图:节点和边是否属于同一类型
- 静态图/动态图:图的结构是否随时间变化
下面用表格总结不同类型图的特点和应用场景:
图类型 | 特点 | 应用场景 | 表示方法 |
---|---|---|---|
无向图 | 边没有方向 | 社交网络中的朋友关系 | 对称邻接矩阵 |
有向图 | 边有方向 | 引用网络、网页链接 | 非对称邻接矩阵 |
带权图 | 边有权重 | 交通网络中的距离 | 权重邻接矩阵 |
异构图 | 多种类型的节点和边 | 知识图谱 | 多关系邻接矩阵 |
动态图 | 随时间变化 | 随时间演化的社交网络 | 时序邻接矩阵序列 |
了解了图的基本概念,下面让我们正式进入图神经网络的世界!
图卷积网络的消息传递机制
消息传递框架
图神经网络的核心思想是消息传递,即每个节点通过聚合邻居节点的信息来更新自己的表示。这一过程可以形式化为:
h v ( k ) = UPDATE ( k ) ( h v ( k − 1 ) , AGGREGATE ( k ) ( { h u ( k − 1 ) : u ∈ N ( v ) } ) ) h_v^{(k)} = \text{UPDATE}^{(k)}\left(h_v^{(k-1)}, \text{AGGREGATE}^{(k)}\left(\{h_u^{(k-1)} : u \in \mathcal{N}(v)\}\right)\right) hv(k)=UPDATE(k)(hv(k−1),AGGREGATE(k)({hu(k−1):u∈N(v)}))
其中:
- h v ( k ) h_v^{(k)} hv(k) 是节点 v v v 在第 k k k 层的隐藏表示
- N ( v ) \mathcal{N}(v) N(v) 是节点 v v v 的邻居集合
- AGGREGATE 函数聚合邻居节点的信息
- UPDATE 函数更新节点自身的表示
这一框架如此优雅,它允许我们设计各种不同的聚合和更新函数,从而产生不同的GNN变体。让我们通过一个简单的例子来理解这一过程:
想象一个简单的社交网络,其中每个人(节点)有自己的特征(如年龄、兴趣爱好等)。通过消息传递,每个人可以了解到朋友的信息,从而更新自己对社交圈的理解。经过多轮消息传递,每个人都能获得一个融合了社交网络结构和个人特征的表示。
图卷积网络(GCN)的消息传递
图卷积网络(Graph Convolutional Network, GCN)是最经典的GNN模型之一,它的消息传递机制可以表示为:
h v ( k ) = σ ( ∑ u ∈ N ( v ) ∪ { v } 1 ∣ N ( v ) ∣ ⋅ ∣ N ( u ) ∣ ⋅ W ( k ) ⋅ h u ( k − 1 ) ) h_v^{(k)} = \sigma\left(\sum_{u \in \mathcal{N}(v) \cup \{v\}} \frac{1}{\sqrt{|\mathcal{N}(v)|} \cdot \sqrt{|\mathcal{N}(u)|}} \cdot W^{(k)} \cdot h_u^{(k-1)}\right) hv(k)=σ u∈N(v)∪{v}∑∣N(v)∣⋅∣N(u)∣1⋅W(k)⋅hu(k−1)
这个公式看起来有点复杂,我们可以拆解开来理解:
- 邻居聚合: ∑ u ∈ N ( v ) ∪ { v } \sum_{u \in \mathcal{N}(v) \cup \{v\}} ∑u∈N(v)∪{v} 表示聚合节点 v v v 自身和其所有邻居的表示
- 归一化: 1 ∣ N ( v ) ∣ ⋅ ∣ N ( u ) ∣ \frac{1}{\sqrt{|\mathcal{N}(v)|} \cdot \sqrt{|\mathcal{N}(u)|}} ∣N(v)∣⋅∣N(u)∣1 是归一化因子,防止度数大的节点主导聚合过程
- 线性变换: W ( k ) ⋅ h u ( k − 1 ) W^{(k)} \cdot h_u^{(k-1)} W(k)⋅hu(k−1) 将节点表示投影到新的特征空间
- 非线性激活: σ ( ⋅ ) \sigma(\cdot) σ(⋅) 是非线性激活函数,如ReLU
用矩阵形式表示,GCN的一层传播规则为:
H ( k ) = σ ( D ~ − 1 2 A ~ D ~ − 1 2 H ( k − 1 ) W ( k ) ) H^{(k)} = \sigma(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(k-1)} W^{(k)}) H(k)=σ(D~−21A~D~−21H(k−1)W(k))
其中:
- A ~ = A + I N \tilde{A} = A + I_N A~=A+IN 是添加了自环的邻接矩阵( I N I_N IN 是单位矩阵)
- D ~ \tilde{D} D~ 是 A ~ \tilde{A} A~ 的度矩阵, D ~ i i = ∑ j A ~ i j \tilde{D}_{ii} = \sum_j \tilde{A}_{ij} D~ii=∑jA~ij
- H ( k ) H^{(k)} H(k) 是第 k k k 层所有节点的隐藏表示矩阵
- W ( k ) W^{(k)} W(k) 是第 k k k 层的权重矩阵
消息传递过程示例
让我们通过一个具体的例子来理解GCN的消息传递过程。考虑下面这个小型图:
2
/ \
1---3
\ /
4
- 节点集合: V = { 1 , 2 , 3 , 4 } V = \{1, 2, 3, 4\} V={1,2,3,4}
- 边集合: E = { ( 1 , 2 ) , ( 1 , 3 ) , ( 1 , 4 ) , ( 2 , 3 ) , ( 3 , 4 ) } E = \{(1,2), (1,3), (1,4), (2,3), (3,4)\} E={(1,2),(1,3),(1,4),(2,3),(3,4)}
假设每个节点的初始特征都是2维向量:
- h 1 ( 0 ) = [ 1 , 0 ] h_1^{(0)} = [1, 0] h1(0)=[1,0]
- h 2 ( 0 ) = [ 0 , 1 ] h_2^{(0)} = [0, 1] h2(0)=[0,1]
- h 3 ( 0 ) = [ 1 , 1 ] h_3^{(0)} = [1, 1] h3(0)=[1,1]
- h 4 ( 0 ) = [ 0 , 0 ] h_4^{(0)} = [0, 0] h4(0)=[0,0]
现在我们计算第一层GCN后节点1的表示。节点1的邻居是2、3和4。
邻接矩阵 A A A 和添加自环后的邻接矩阵 A ~ \tilde{A} A~ 为:
A = [ 0 1 1 1 1 0 1 0 1 1 0 1 1 0 1 0 ] , A ~ = [ 1 1 1 1 1 1 1 0 1 1 1 1 1 0 1 1 ] A = \begin{bmatrix} 0 & 1 & 1 & 1 \\ 1 & 0 & 1 & 0 \\ 1 & 1 & 0 & 1 \\ 1 & 0 & 1 & 0 \end{bmatrix}, \tilde{A} = \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 0 \\ 1 & 1 & 1 & 1 \\ 1 & 0 & 1 & 1 \end{bmatrix} A= 0111101011011010 ,A~= 1111111011111011
度矩阵 D ~ \tilde{D} D~ 为:
D ~ = [ 4 0 0 0 0 3 0 0 0 0 4 0 0 0 0 3 ] \tilde{D} = \begin{bmatrix} 4 & 0 & 0 & 0 \\ 0 & 3 & 0 & 0 \\ 0 & 0 & 4 & 0 \\ 0 & 0 & 0 & 3 \end{bmatrix} D~= 4000030000400003
归一化邻接矩阵 D ~ − 1 2 A ~ D ~ − 1 2 \tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} D~−21A~D~−21 为:
D ~ − 1 2 A ~ D ~ − 1 2 = [ 1 4 1 12 1 4 1 12 1 12 1 3 1 12 0 1 4 1 12 1 4 1 12 1 12 0 1 12 1 3 ] \tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} = \begin{bmatrix} \frac{1}{4} & \frac{1}{\sqrt{12}} & \frac{1}{4} & \frac{1}{\sqrt{12}} \\ \frac{1}{\sqrt{12}} & \frac{1}{3} & \frac{1}{\sqrt{12}} & 0 \\ \frac{1}{4} & \frac{1}{\sqrt{12}} & \frac{1}{4} & \frac{1}{\sqrt{12}} \\ \frac{1}{\sqrt{12}} & 0 & \frac{1}{\sqrt{12}} & \frac{1}{3} \end{bmatrix} D~−21A~D~−21= 41121411211213112104112141121121012131
假设我们的权重矩阵 W ( 1 ) ∈ R 2 × 2 W^{(1)} \in \mathbb{R}^{2 \times 2} W(1)∈R2×2 为单位矩阵,激活函数为ReLU。
则节点1的新表示为:
h 1 ( 1 ) = ReLU ( 1 4 ⋅ [ 1 , 0 ] + 1 12 ⋅ [ 0 , 1 ] + 1 4 ⋅ [ 1 , 1 ] + 1 12 ⋅ [ 0 , 0 ] ) h_1^{(1)} = \text{ReLU}\left(\frac{1}{4} \cdot [1, 0] + \frac{1}{\sqrt{12}} \cdot [0, 1] + \frac{1}{4} \cdot [1, 1] + \frac{1}{\sqrt{12}} \cdot [0, 0]\right) h1(1)=ReLU(41⋅[1,0]+121⋅[0,1]+41⋅[1,1]+121⋅[0,0])
h 1 ( 1 ) = ReLU ( [ 1 4 + 0 + 1 4 + 0 , 0 + 1 12 + 1 4 + 0 ] ) h_1^{(1)} = \text{ReLU}\left(\left[\frac{1}{4} + 0 + \frac{1}{4} + 0, 0 + \frac{1}{\sqrt{12}} + \frac{1}{4} + 0\right]\right) h1(1)=ReLU([41+0+41+0,0+121+41+0])
h 1 ( 1 ) = ReLU ( [ 1 2 , 1 12 + 1 4 ] ) h_1^{(1)} = \text{ReLU}\left(\left[\frac{1}{2}, \frac{1}{\sqrt{12}} + \frac{1}{4}\right]\right) h1(1)=ReLU([21,121+41])
这样,我们就完成了节点1在第一层的消息传递。通过多层消息传递,节点能够获取更大范围内的结构信息。
谱域与空域方法对比
图卷积网络可以从两个角度来定义:谱域方法和空域方法。这两种方法各有优缺点,下面我们对它们进行深入对比。
谱域方法
谱域方法基于图信号处理理论,将图卷积定义为图傅里叶域中的乘积。
图拉普拉斯矩阵
图拉普拉斯矩阵是谱域方法的核心,定义为:
L = D − A L = D - A L=D−A
其中 D D D 是度矩阵, A A A 是邻接矩阵。
归一化的拉普拉斯矩阵为:
L s y m = D − 1 2 L D − 1 2 = I − D − 1 2 A D − 1 2 L_{sym} = D^{-\frac{1}{2}} L D^{-\frac{1}{2}} = I - D^{-\frac{1}{2}} A D^{-\frac{1}{2}} Lsym=D−21LD−21=I−D−21AD−21
拉普拉斯矩阵有许多良好的性质:
- 它是对称半正定矩阵
- 它的特征值和特征向量提供了图结构的重要信息
- 最小特征值为0,对应的特征向量与图的连通分量有关
谱域图卷积
设 L = U Λ U T L = U \Lambda U^T L=UΛUT 是拉普拉斯矩阵的特征分解,其中 U U U 是特征向量矩阵, Λ \Lambda Λ 是特征值对角矩阵。
给定信号 x ∈ R N x \in \mathbb{R}^N x∈RN,其图傅里叶变换定义为:
x ^ = U T x \hat{x} = U^T x x^=UTx
逆变换为:
x = U x ^ x = U \hat{x} x=Ux^
图卷积定义为信号 x x x 与滤波器 g g g 在傅里叶域的乘积:
x ∗ G g = U ( ( U T x ) ⊙ ( U T g ) ) = U diag ( g ^ ) U T x x *_G g = U((U^T x) \odot (U^T g)) = U \text{diag}(\hat{g}) U^T x x∗Gg=U((UTx)⊙(UTg))=Udiag(g^)UTx
其中 g ^ \hat{g} g^ 是滤波器 g g g 的傅里叶变换。
切比雪夫多项式近似
直接计算特征分解计算成本高,因此通常使用切比雪夫多项式来近似滤波器:
g θ ( Λ ) ≈ ∑ k = 0 K θ k T k ( Λ ~ ) g_{\theta}(\Lambda) \approx \sum_{k=0}^{K} \theta_k T_k(\tilde{\Lambda}) gθ(Λ)≈k=0∑KθkTk(Λ~)
其中 T k T_k Tk 是k阶切比雪夫多项式, Λ ~ = 2 Λ / λ m a x − I \tilde{\Lambda} = 2\Lambda/\lambda_{max} - I Λ~=2Λ/λmax−I。
这样,图卷积可以表示为:
g θ ∗ x ≈ ∑ k = 0 K θ k T k ( L ~ ) x g_{\theta} * x \approx \sum_{k=0}^{K} \theta_k T_k(\tilde{L}) x gθ∗x≈k=0∑KθkTk(L~)x
其中 L ~ = 2 L / λ m a x − I \tilde{L} = 2L/\lambda_{max} - I L~=2L/λmax−I。
空域方法
空域方法更直观,直接在图的节点和边上定义卷积操作,不需要进行图傅里叶变换。
消息传递框架
空域方法通常基于消息传递框架,节点通过聚合邻居信息来更新表示:
h v ( k ) = ϕ ( h v ( k − 1 ) , □ u ∈ N ( v ) ψ ( h v ( k − 1 ) , h u ( k − 1 ) , e v u ) ) h_v^{(k)} = \phi\left(h_v^{(k-1)}, \square_{u \in \mathcal{N}(v)} \psi(h_v^{(k-1)}, h_u^{(k-1)}, e_{vu})\right) hv(k)=ϕ(hv(k−1),□u∈N(v)ψ(hv(k−1),hu(k−1),evu))
其中 □ \square □ 是可微的置换不变函数(如求和、平均、最大值等), ϕ \phi ϕ 和 ψ \psi ψ 是可学习的函数。
GraphSAGE
GraphSAGE是一种典型的空域方法,它的更新规则为:
h v ( k ) = σ ( W ( k ) ⋅ CONCAT ( h v ( k − 1 ) , AGG ( { h u ( k − 1 ) , ∀ u ∈ N ( v ) } ) ) ) h_v^{(k)} = \sigma\left(W^{(k)} \cdot \text{CONCAT}(h_v^{(k-1)}, \text{AGG}(\{h_u^{(k-1)}, \forall u \in \mathcal{N}(v)\}))\right) hv(k)=σ(W(k)⋅CONCAT(hv(k−1),AGG({hu(k−1),∀u∈N(v)})))
其中AGG可以是均值聚合、LSTM聚合或池化聚合。
谱域与空域方法对比表
下面用表格总结谱域方法和空域方法的主要差异:
特性 | 谱域方法 | 空域方法 |
---|---|---|
理论基础 | 图信号处理、图傅里叶变换 | 直接在图结构上定义的卷积 |
计算复杂度 | 高(需要特征分解)或中等(使用多项式近似) | 低(直接聚合邻居信息) |
适用性 | 适用于固定图结构 | 适用于动态变化的图 |
表达能力 | 可以设计复杂的频域滤波器 | 依赖于聚合函数的设计 |
可解释性 | 基于频域解释,不直观 | 基于消息传递,较直观 |
代表模型 | ChebNet、GCN | GraphSAGE、GAT |
归纳能力 | 弱(难以泛化到新图结构) | 强(易于泛化到新图结构) |
从谱域到空域的桥梁:GCN
有趣的是,GCN模型可以同时从谱域和空域解释:
- 从谱域角度,GCN使用一阶切比雪夫多项式近似滤波器
- 从空域角度,GCN通过归一化的邻居聚合来更新节点表示
GCN的更新规则:
H ( k ) = σ ( D ~ − 1 2 A ~ D ~ − 1 2 H ( k − 1 ) W ( k ) ) H^{(k)} = \sigma(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(k-1)} W^{(k)}) H(k)=σ(D~−21A~D~−21H(k−1)W(k))
这使得GCN成为连接谱域和空域方法的重要桥梁。
节点嵌入的聚合公式推导
在图神经网络中,节点嵌入的聚合是核心操作。下面我们推导几种常见的聚合公式。
GCN的聚合公式
GCN的聚合公式可以从谱域推导得出。首先,考虑图信号 x x x 上的卷积:
g θ ∗ x = U g θ ( Λ ) U T x g_{\theta} * x = U g_{\theta}(\Lambda) U^T x gθ∗x=Ugθ(Λ)UTx
使用一阶切比雪夫多项式近似 g θ ( Λ ) ≈ θ 0 + θ 1 ( 2 Λ / λ m a x − I ) g_{\theta}(\Lambda) \approx \theta_0 + \theta_1 (2\Lambda/\lambda_{max} - I) gθ(Λ)≈θ0+θ1(2Λ/λmax−I),并设 θ 0 = θ 1 \theta_0 = \theta_1 θ0=θ1 和 λ m a x = 2 \lambda_{max} = 2 λmax=2,我们得到:
g θ ∗ x ≈ θ 0 ( I − D − 1 2 A D − 1 2 ) x = θ 0 D − 1 2 ( D − A ) D − 1 2 x g_{\theta} * x \approx \theta_0(I - D^{-\frac{1}{2}} A D^{-\frac{1}{2}})x = \theta_0 D^{-\frac{1}{2}} (D - A) D^{-\frac{1}{2}} x gθ∗x≈θ0(I−D−21AD−21)x=θ0D−21(D−A)D−21x
为了避免梯度消失/爆炸问题,添加自环并重新归一化:
g θ ∗ x ≈ θ 0 D ~ − 1 2 A ~ D ~ − 1 2 x g_{\theta} * x \approx \theta_0 \tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} x gθ∗x≈θ0D~−21A~D~−21x
其中 A ~ = A + I \tilde{A} = A + I A~=A+I 和 D ~ i i = ∑ j A ~ i j \tilde{D}_{ii} = \sum_j \tilde{A}_{ij} D~ii=∑jA~ij。
这样,GCN的层间传播规则为:
H ( k ) = σ ( D ~ − 1 2 A ~ D ~ − 1 2 H ( k − 1 ) W ( k ) ) H^{(k)} = \sigma(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(k-1)} W^{(k)}) H(k)=σ(D~−21A~D~−21H(k−1)W(k))
对于单个节点 v v v,聚合公式为:
h v ( k ) = σ ( ∑ u ∈ N ( v ) ∪ { v } 1 d ~ v d ~ u h u ( k − 1 ) W ( k ) ) h_v^{(k)} = \sigma\left(\sum_{u \in \mathcal{N}(v) \cup \{v\}} \frac{1}{\sqrt{\tilde{d}_v \tilde{d}_u}} h_u^{(k-1)} W^{(k)}\right) hv(k)=σ u∈N(v)∪{v}∑d~vd~u1hu(k−1)W(k)
其中 d ~ v \tilde{d}_v d~v 是节点 v v v 添加自环后的度。
GraphSAGE的聚合公式
GraphSAGE提出了多种聚合函数,下面我们推导其中的均值聚合:
- 邻居聚合: h N ( v ) ( k ) = 1 ∣ N ( v ) ∣ ∑ u ∈ N ( v ) h u ( k − 1 ) h_{\mathcal{N}(v)}^{(k)} = \frac{1}{|\mathcal{N}(v)|} \sum_{u \in \mathcal{N}(v)} h_u^{(k-1)} hN(v)(k)=∣N(v)∣1∑u∈N(v)hu(k−1)
- 拼接自身表示: z v ( k ) = [ h v ( k − 1 ) ∥ h N ( v ) ( k ) ] z_v^{(k)} = [h_v^{(k-1)} \| h_{\mathcal{N}(v)}^{(k)}] zv(k)=[hv(k−1)∥hN(v)(k)]
- 线性变换和激活: h v ( k ) = σ ( W ( k ) ⋅ z v ( k ) ) h_v^{(k)} = \sigma(W^{(k)} \cdot z_v^{(k)}) hv(k)=σ(W(k)⋅zv(k))
- L2正则化: h v ( k ) = h v ( k ) / ∥ h v ( k ) ∥ 2 h_v^{(k)} = h_v^{(k)} / \|h_v^{(k)}\|_2 hv(k)=hv(k)/∥hv(k)∥2
完整的均值聚合公式为:
h v ( k ) = σ ( W ( k ) ⋅ [ h v ( k − 1 ) ∥ 1 ∣ N ( v ) ∣ ∑ u ∈ N ( v ) h u ( k − 1 ) ] ) h_v^{(k)} = \sigma\left(W^{(k)} \cdot \left[h_v^{(k-1)} \Big\| \frac{1}{|\mathcal{N}(v)|} \sum_{u \in \mathcal{N}(v)} h_u^{(k-1)}\right]\right) hv(k)=σ W(k)⋅ hv(k−1) ∣N(v)∣1u∈N(v)∑hu(k−1)
GAT的注意力聚合公式
图注意力网络(GAT)引入了注意力机制来加权聚合邻居信息:
- 计算注意力系数: e v u = a ( W h v , W h u ) e_{vu} = a(W h_v, W h_u) evu=a(Whv,Whu),其中 a a a 是注意力函数
- 归一化注意力系数: α v u = softmax u ( e v u ) = exp ( e v u ) ∑ w ∈ N ( v ) exp ( e v w ) \alpha_{vu} = \text{softmax}_u(e_{vu}) = \frac{\exp(e_{vu})}{\sum_{w \in \mathcal{N}(v)} \exp(e_{vw})} αvu=softmaxu(evu)=∑w∈N(v)exp(evw)exp(evu)
- 加权聚合: h v ( k ) = σ ( ∑ u ∈ N ( v ) α v u W h u ( k − 1 ) ) h_v^{(k)} = \sigma\left(\sum_{u \in \mathcal{N}(v)} \alpha_{vu} W h_u^{(k-1)}\right) hv(k)=σ(∑u∈N(v)αvuWhu(k−1))
具体地,使用LeakyReLU作为激活函数的单头注意力机制为:
e v u = LeakyReLU ( a ⃗ T [ W h v ∥ W h u ] ) e_{vu} = \text{LeakyReLU}(\vec{a}^T [W h_v \| W h_u]) evu=LeakyReLU(aT[Whv∥Whu])
多头注意力机制为:
h v ( k ) = ∥ i = 1 K σ ( ∑ u ∈ N ( v ) α v u i W i h u ( k − 1 ) ) h_v^{(k)} = \Big\|_{i=1}^{K} \sigma\left(\sum_{u \in \mathcal{N}(v)} \alpha_{vu}^i W^i h_u^{(k-1)}\right) hv(k)= i=1Kσ u∈N(v)∑αvuiWihu(k−1)
其中 ∥ \| ∥ 表示拼接, K K K 是注意力头数。
聚合函数对比分析
下表总结了不同GNN模型的聚合函数特点:
模型 | 聚合函数 | 优点 | 缺点 |
---|---|---|---|
GCN | ∑ u ∈ N ( v ) ∪ { v } 1 d ~ v d ~ u h u W \sum_{u \in \mathcal{N}(v) \cup \{v\}} \frac{1}{\sqrt{\tilde{d}_v \tilde{d}_u}} h_u W ∑u∈N(v)∪{v}d~vd~u1huW | 简单有效,考虑了节点度 | 所有邻居权重相同 |
GraphSAGE | σ ( W [ h v ∣ AGG ( { h u , ∀ u ∈ N ( v ) } ) ] ) \sigma(W [h_v | \text{AGG}(\{h_u, \forall u \in \mathcal{N}(v)\})]) σ(W[hv∣AGG({hu,∀u∈N(v)})]) | 灵活多样,保留中心节点信息 | 计算复杂度较高 |
GAT | σ ( ∑ u ∈ N ( v ) α v u W h u ) \sigma(\sum_{u \in \mathcal{N}(v)} \alpha_{vu} W h_u) σ(∑u∈N(v)αvuWhu) | 自适应学习边的重要性 | 需要额外的注意力计算 |
GIN | MLP ( ( 1 + ϵ ) ⋅ h v + ∑ u ∈ N ( v ) h u ) \text{MLP}((1+\epsilon) \cdot h_v + \sum_{u \in \mathcal{N}(v)} h_u) MLP((1+ϵ)⋅hv+∑u∈N(v)hu) | 最具表达能力,等价于WL测试 | 参数较多,容易过拟合 |
PyTorch实现图卷积网络
下面我们使用PyTorch和PyTorch Geometric库实现一个图卷积网络,用于节点分类任务。
环境准备
首先,安装必要的库:
# 安装PyTorch Geometric
# pip install torch-scatter torch-sparse torch-cluster torch-spline-conv torch-geometric
数据集准备
我们使用Cora数据集,它是一个引用网络数据集,包含2708个科学出版物节点和5429条引用边。每个出版物分为7个类别之一,每个出版物节点有1433个词袋特征。
import torch
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
# 加载Cora数据集
dataset = Planetoid(root='./data/Cora', name='Cora', transform=NormalizeFeatures())
data = dataset[0]
print(f'数据集信息:')
print(f' 节点数: {data.num_nodes}')
print(f' 边数: {data.num_edges // 2}') # 无向图,边数除以2
print(f' 节点特征维度: {data.num_features}')
print(f' 类别数: {dataset.num_classes}')
print(f' 训练节点数: {data.train_mask.sum().item()}')
print(f' 验证节点数: {data.val_mask.sum().item()}')
print(f' 测试节点数: {data.test_mask.sum().item()}')
实现GCN模型
下面实现一个两层的GCN模型:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
class GCN(nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels, dropout=0.5):
super(GCN, self).__init__()
self.conv1 = GCNConv(in_channels, hidden_channels)
self.conv2 = GCNConv(hidden_channels, out_channels)
self.dropout = dropout
def forward(self, x, edge_index):
# 第一层GCN
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
# 第二层GCN
x = self.conv2(x, edge_index)
return x
def reset_parameters(self):
self.conv1.reset_parameters()
self.conv2.reset_parameters()
PyTorch实现图卷积网络
手动实现GCN层
为了更好地理解GCN的工作原理,我们也手动实现一个GCN层:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_sparse import SparseTensor, matmul
class CustomGCNConv(nn.Module):
def __init__(self, in_channels, out_channels):
super(CustomGCNConv, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
# 线性变换权重
self.weight = nn.Parameter(torch.Tensor(in_channels, out_channels))
self.bias = nn.Parameter(torch.Tensor(out_channels))
self.reset_parameters()
def reset_parameters(self):
nn.init.xavier_uniform_(self.weight)
nn.init.zeros_(self.bias)
def forward(self, x, edge_index):
# 步骤1: 线性变换
x = torch.matmul(x, self.weight)
# 步骤2: 计算归一化系数
row, col = edge_index
deg = torch.bincount(row, minlength=x.size(0))
deg_inv_sqrt = deg.pow(-0.5)
deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0
# 步骤3: 消息传递
out = torch.zeros_like(x)
for i in range(edge_index.size(1)):
# 获取边的源节点和目标节点
src, dst = edge_index[0, i], edge_index[1, i]
# 计算归一化系数
norm = deg_inv_sqrt[src] * deg_inv_sqrt[dst]
# 更新目标节点的表示
out[dst] += norm * x[src]
# 步骤4: 添加偏置
out += self.bias
return out
训练模型
下面我们实现训练和评估函数:
def train(model, data, optimizer):
model.train()
optimizer.zero_grad()
# 前向传播
out = model(data.x, data.edge_index)
# 计算训练损失
loss = F.cross_entropy(out[data.train_mask], data.y[data.train_mask])
# 反向传播
loss.backward()
optimizer.step()
return loss.item()
def test(model, data):
model.eval()
with torch.no_grad():
out = model(data.x, data.edge_index)
# 训练集、验证集和测试集的预测
pred = out.argmax(dim=1)
# 计算准确率
train_acc = pred[data.train_mask].eq(data.y[data.train_mask]).sum().item() / data.train_mask.sum().item()
val_acc = pred[data.val_mask].eq(data.y[data.val_mask]).sum().item() / data.val_mask.sum().item()
test_acc = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item() / data.test_mask.sum().item()
return train_acc, val_acc, test_acc
完整的训练和评估流程
现在,我们将所有内容整合起来,实现完整的训练和评估流程:
import torch
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
import matplotlib.pyplot as plt
import numpy as np
# 设置随机种子,确保结果可复现
torch.manual_seed(42)
# 加载Cora数据集
dataset = Planetoid(root='./data/Cora', name='Cora', transform=NormalizeFeatures())
data = dataset[0]
# 初始化GCN模型
model = GCN(in_channels=dataset.num_features,
hidden_channels=16,
out_channels=dataset.num_classes)
# 使用Adam优化器
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
# 训练参数
num_epochs = 200
best_val_acc = 0
best_test_acc = 0
# 记录损失和准确率
train_losses = []
train_accs = []
val_accs = []
test_accs = []
# 训练循环
for epoch in range(num_epochs):
# 训练一个轮次
loss = train(model, data, optimizer)
train_losses.append(loss)
# 评估模型
train_acc, val_acc, test_acc = test(model, data)
train_accs.append(train_acc)
val_accs.append(val_acc)
test_accs.append(test_acc)
# 保存最佳模型
if val_acc > best_val_acc:
best_val_acc = val_acc
best_test_acc = test_acc
# 打印训练进度
if (epoch+1) % 10 == 0:
print(f'Epoch: {epoch+1:03d}, Loss: {loss:.4f}, Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}, Test Acc: {test_acc:.4f}')
print(f'最佳验证集准确率: {best_val_acc:.4f}')
print(f'对应的测试集准确率: {best_test_acc:.4f}')
# 绘制训练曲线
plt.figure(figsize=(12, 5))
# 损失曲线
plt.subplot(1, 2, 1)
plt.plot(train_losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
# 准确率曲线
plt.subplot(1, 2, 2)
plt.plot(train_accs, label='Train')
plt.plot(val_accs, label='Validation')
plt.plot(test_accs, label='Test')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Accuracy')
plt.legend()
plt.tight_layout()
plt.savefig('gcn_training_curves.png')
plt.show()
成功训练后,我们可以从训练曲线中看到模型逐渐收敛,在Cora数据集上GCN模型通常能达到80%以上的测试准确率。
代码运行流程与可视化
为了更直观地理解GCN的工作原理,下面我们可视化模型的消息传递过程和节点嵌入。
消息传递流程图
下面是GCN消息传递的流程图:
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ 节点特征 X │ │ 归一化邻接矩阵 │ │ 权重矩阵 W │
│ │ │ │ │ │
│ Node 1: [x1] │ │ [A_norm] │ │ [W_matrix] │
│ Node 2: [x2] │ │ │ │ │
│ ... │ │ │ │ │
│ Node n: [xn] │ │ │ │ │
└─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
│ │ │
▼ │ │
┌───────────────────┐ │ │
│ 特征传播 │◄────────────────┘ │
│ │ │
│ X' = A_norm * X │ │
└─────────┬─────────┘ │
│ │
│ │
▼ │
┌───────────────────┐ │
│ 特征变换 │◄────────────────────────────────────────────┘
│ │
│ Z = X' * W │
└─────────┬─────────┘
│
│
▼
┌───────────────────┐
│ 非线性激活 │
│ │
│ H = ReLU(Z) │
└─────────┬─────────┘
│
│
▼
┌───────────────────┐
│ Dropout │
│ │
│ H_drop = Dropout(H) │
└───────────────────┘
可视化节点嵌入
我们还可以使用t-SNE算法可视化GCN提取的节点嵌入,看看不同类别的节点是否被很好地分开:
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
import numpy as np
def visualize_embeddings(model, data):
model.eval()
with torch.no_grad():
# 获取最后一层GCN前的节点嵌入
x = model.conv1(data.x, data.edge_index)
x = F.relu(x)
# 使用t-SNE降维到2D空间
tsne = TSNE(n_components=2, random_state=42)
x_2d = tsne.fit_transform(x.detach().numpy())
# 获取节点类别
y = data.y.numpy()
# 绘制嵌入
plt.figure(figsize=(10, 8))
for i in range(dataset.num_classes):
mask = y == i
plt.scatter(x_2d[mask, 0], x_2d[mask, 1], label=f'Class {i}')
plt.legend()
plt.title('Node Embeddings Visualization using t-SNE')
plt.savefig('node_embeddings.png')
plt.show()
# 训练完成后可视化
visualize_embeddings(model, data)
注意力权重可视化(对于GAT)
如果使用GAT模型,我们可以可视化注意力权重,看看模型如何分配注意力:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GATConv
import networkx as nx
import matplotlib.pyplot as plt
class GAT(nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels, heads=8, dropout=0.6):
super(GAT, self).__init__()
self.conv1 = GATConv(in_channels, hidden_channels, heads=heads, dropout=dropout)
# 输出层使用单头注意力
self.conv2 = GATConv(hidden_channels * heads, out_channels, heads=1, concat=False, dropout=dropout)
self.dropout = dropout
def forward(self, x, edge_index):
x = F.dropout(x, p=self.dropout, training=self.training)
x = F.elu(self.conv1(x, edge_index))
x = F.dropout(x, p=self.dropout, training=self.training)
x = self.conv2(x, edge_index)
return x
def get_attention_weights(self, x, edge_index):
# 获取第一层的注意力权重
_, attention_weights = self.conv1(x, edge_index, return_attention_weights=True)
return attention_weights
def visualize_attention(model, data, num_nodes=10):
model.eval()
# 获取注意力权重
edge_index, attention_weights = model.get_attention_weights(data.x, data.edge_index)
attention_weights = attention_weights.mean(dim=1).detach().numpy() # 对多头注意力取平均
# 创建子图,只包含前num_nodes个节点
subset = list(range(num_nodes))
sub_edge_index = []
sub_weights = []
for i, (src, dst) in enumerate(edge_index.t().numpy()):
if src in subset and dst in subset:
sub_edge_index.append((src, dst))
sub_weights.append(attention_weights[i])
# 创建图
G = nx.DiGraph()
for node in subset:
G.add_node(node, label=str(node), color=data.y[node].item())
# 添加边
for i, (src, dst) in enumerate(sub_edge_index):
G.add_edge(src, dst, weight=sub_weights[i])
# 获取节点颜色
node_colors = [plt.cm.tab10(G.nodes[node]['color']) for node in G.nodes]
# 获取边权重
edge_weights = [G[u][v]['weight'] * 5 for u, v in G.edges]
# 绘制图
plt.figure(figsize=(10, 8))
pos = nx.spring_layout(G, seed=42)
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=300)
nx.draw_networkx_labels(G, pos)
nx.draw_networkx_edges(G, pos, width=edge_weights, alpha=0.7,
edge_color=edge_weights, edge_cmap=plt.cm.Blues)
plt.title('GAT Attention Weights Visualization')
plt.axis('off')
plt.savefig('attention_weights.png')
plt.show()
实验与结果分析
在这一部分,我们将比较不同GNN模型在节点分类任务上的性能,并分析实验结果。
实验设置
我们在Cora数据集上比较以下模型:
- 多层感知机(MLP):仅使用节点特征,不考虑图结构
- 图卷积网络(GCN):综合考虑节点特征和图结构
- 图注意力网络(GAT):使用注意力机制加权聚合邻居信息
- GraphSAGE:使用不同的聚合函数
所有模型都使用两层神经网络,隐藏层维度为16,训练200轮。以下是实验代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GCNConv, GATConv, SAGEConv
from torch_geometric.datasets import Planetoid
import matplotlib.pyplot as plt
import numpy as np
import time
# 设置随机种子
torch.manual_seed(42)
# 加载Cora数据集
dataset = Planetoid(root='./data/Cora', name='Cora', transform=NormalizeFeatures())
data = dataset[0]
# MLP模型
class MLP(nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels, dropout=0.5):
super(MLP, self).__init__()
self.lin1 = nn.Linear(in_channels, hidden_channels)
self.lin2 = nn.Linear(hidden_channels, out_channels)
self.dropout = dropout
def forward(self, x, edge_index=None):
x = F.dropout(x, p=self.dropout, training=self.training)
x = F.relu(self.lin1(x))
x = F.dropout(x, p=self.dropout, training=self.training)
x = self.lin2(x)
return x
# GCN, GAT和GraphSAGE模型定义同前面的代码
# 实验函数
def run_experiment(model_name, model, data, epochs=200, lr=0.01, weight_decay=5e-4):
optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
best_val_acc = 0
best_test_acc = 0
train_losses = []
train_accs = []
val_accs = []
test_accs = []
start_time = time.time()
for epoch in range(epochs):
# 训练
model.train()
optimizer.zero_grad()
out = model(data.x, data.edge_index)
loss = F.cross_entropy(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
train_losses.append(loss.item())
# 评估
model.eval()
with torch.no_grad():
pred = out.argmax(dim=1)
train_acc = pred[data.train_mask].eq(data.y[data.train_mask]).sum().item() / data.train_mask.sum().item()
val_acc = pred[data.val_mask].eq(data.y[data.val_mask]).sum().item() / data.val_mask.sum().item()
test_acc = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item() / data.test_mask.sum().item()
train_accs.append(train_acc)
val_accs.append(val_acc)
test_accs.append(test_acc)
if val_acc > best_val_acc:
best_val_acc = val_acc
best_test_acc = test_acc
end_time = time.time()
training_time = end_time - start_time
print(f'{model_name}:')
print(f' 最佳验证集准确率: {best_val_acc:.4f}')
print(f' 对应的测试集准确率: {best_test_acc:.4f}')
print(f' 训练时间: {training_time:.2f} 秒')
return {
'model_name': model_name,
'train_losses': train_losses,
'train_accs': train_accs,
'val_accs': val_accs,
'test_accs': test_accs,
'best_val_acc': best_val_acc,
'best_test_acc': best_test_acc,
'training_time': training_time
}
# 运行实验
results = []
# MLP
model = MLP(in_channels=dataset.num_features, hidden_channels=16, out_channels=dataset.num_classes)
results.append(run_experiment('MLP', model, data))
# GCN
model = GCN(in_channels=dataset.num_features, hidden_channels=16, out_channels=dataset.num_classes)
results.append(run_experiment('GCN', model, data))
# GAT
model = GAT(in_channels=dataset.num_features, hidden_channels=8, out_channels=dataset.num_classes)
results.append(run_experiment('GAT', model, data))
# GraphSAGE
model = GraphSAGE(in_channels=dataset.num_features, hidden_channels=16, out_channels=dataset.num_classes)
results.append(run_experiment('GraphSAGE', model, data))
# 绘制结果
def plot_results(results):
plt.figure(figsize=(15, 10))
# 损失曲线
plt.subplot(2, 2, 1)
for result in results:
plt.plot(result['train_losses'], label=result['model_name'])
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()
# 训练准确率
plt.subplot(2, 2, 2)
for result in results:
plt.plot(result['train_accs'], label=result['model_name'])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training Accuracy')
plt.legend()
# 验证准确率
plt.subplot(2, 2, 3)
for result in results:
plt.plot(result['val_accs'], label=result['model_name'])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Validation Accuracy')
plt.legend()
# 测试准确率
plt.subplot(2, 2, 4)
for result in results:
plt.plot(result['test_accs'], label=result['model_name'])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Test Accuracy')
plt.legend()
plt.tight_layout()
plt.savefig('model_comparison.png')
plt.show()
# 绘制模型比较柱状图
plt.figure(figsize=(10, 6))
model_names = [result['model_name'] for result in results]
test_accs = [result['best_test_acc'] for result in results]
training_times = [result['training_time'] for result in results]
x = np.arange(len(model_names))
width = 0.35
fig, ax1 = plt.subplots(figsize=(10, 6))
ax2 = ax1.twinx()
bars1 = ax1.bar(x - width/2, test_accs, width, label='Test Accuracy', color='skyblue')
bars2 = ax2.bar(x + width/2, training_times, width, label='Training Time (s)', color='salmon')
ax1.set_xlabel('Model')
ax1.set_ylabel('Test Accuracy')
ax2.set_ylabel('Training Time (s)')
ax1.set_title('Model Performance Comparison')
ax1.set_xticks(x)
ax1.set_xticklabels(model_names)
ax1.legend(loc='upper left')
ax2.legend(loc='upper right')
plt.tight_layout()
plt.savefig('model_performance.png')
plt.show()
# 绘制结果
plot_results(results)
实验结果与分析
通过实验,我们对比了不同GNN模型的性能,结果通常如下:
-
准确率对比:
- MLP:~70%(仅使用节点特征)
- GCN:~81%(利用图结构信息)
- GAT:~83%(注意力机制带来提升)
- GraphSAGE:~80%(灵活的聚合函数)
-
训练时间对比:
- MLP:最快(不需要处理图结构)
- GCN:中等(简单的邻居聚合)
- GAT:最慢(注意力机制增加计算量)
- GraphSAGE:较慢(复杂的聚合操作)
-
模型收敛性:
- GAT通常收敛较慢但最终性能更好
- GCN收敛速度适中,性能稳定
- GraphSAGE的收敛行为取决于聚合函数的选择
从实验结果可以看出:
-
利用图结构信息(GCN、GAT、GraphSAGE)比仅使用节点特征(MLP)效果更好,证明了图结构在节点分类任务中的重要性。
-
注意力机制(GAT)能够自适应地学习边的重要性,在异构图或噪声较大的图上尤为有效。
-
不同的聚合函数(GraphSAGE)适用于不同的图结构和任务,需要根据具体场景选择。
-
模型复杂度与性能之间存在权衡,更复杂的模型(如GAT)可能性能更好但训练更慢。
总结与进阶方向
在今天的学习中,我们深入探讨了图神经网络的基础知识,特别是图卷积网络的消息传递机制,对比了谱域与空域方法的计算差异,推导了节点嵌入的聚合公式,并通过PyTorch实现了GCN模型。
主要学习要点回顾
-
图的基本概念与表示:
- 图由节点和边组成,可以用邻接矩阵、邻接表等方式表示
- 图的类型包括无向图/有向图、带权图/无权图、同构图/异构图等
-
图卷积网络的消息传递机制:
- 消息传递是GNN的核心思想,节点通过聚合邻居信息来更新自己的表示
- GCN使用归一化的邻居聚合来传递信息
-
谱域与空域方法对比:
- 谱域方法基于图信号处理理论,将图卷积定义为图傅里叶域中的乘积
- 空域方法直接在图的节点和边上定义卷积操作,更直观且计算效率更高
- GCN可以同时从谱域和空域解释,是连接两种方法的桥梁
-
节点嵌入的聚合公式推导:
- GCN聚合:归一化的邻居加权求和
- GraphSAGE聚合:灵活的聚合函数(均值、最大值、LSTM等)
- GAT聚合:基于注意力机制的加权聚合
-
PyTorch实现与实验分析:
- 使用PyTorch和PyTorch Geometric实现GNN模型
- 在节点分类任务上比较不同GNN模型的性能
- 可视化节点嵌入和注意力权重,帮助理解模型行为
进阶方向
-
更复杂的GNN架构:
- 图Transformer:结合Transformer架构的注意力机制
- 图神经常微分方程(GNODE):连续深度的图神经网络
- 深度图生成模型:如图变分自编码器(GVAE)和图生成对抗网络(GraphGAN)
-
大规模图学习:
- 图采样技术:Neighbor Sampling、GraphSAINT等
- 分布式GNN训练:如DistDGL、PyTorch Geometric Temporal等
- 图神经网络压缩:模型量化、知识蒸馏等
-
图神经网络的可解释性:
- GNN Explainer:解释GNN的预测
- 注意力可视化:理解模型关注的图结构部分
- 子图挖掘:寻找对预测最有影响的子图结构
-
图神经网络的鲁棒性与公平性:
- 对抗攻击与防御:增强GNN对结构扰动的鲁棒性
- 去偏技术:减少GNN中的社会偏见
- 不确定性估计:量化GNN预测的可靠性
-
结合领域知识的图学习:
- 分子图学习:结合化学知识的GNN
- 知识图谱推理:融合逻辑规则的GNN
- 脑网络分析:结合神经科学知识的GNN
清华大学全五版的《DeepSeek教程》完整的文档需要的朋友,关注我私信:deepseek 即可获得。
怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!