文章目录
- 图基本知识
- GNN简介
- GCN
- PYG极简入门
- Data Handling of Graphs
- Common Benchmark Datasets
- Mini-batches
- Data Transforms
- Learning Methods on Graphs
图基本知识
😸图是由节点的有穷非空集合和节点之间边的集合组成,通常表示为 G = ( V , E ) G=(V, E) G=(V,E),其可分为有向图和无向图,无向图中节点之间的边没有方向,而有向图中的边是有方向的。对于图的通用符号定义中, G G G 表示一个图, V V V 为图中的节点集合, E E E 为图中的边集合,节点数量 n = ∣ V ∣ n=|V| n=∣V∣,边数量 e = ∣ E ∣ e = |E| e=∣E∣,节点 v i ∈ V v_i \in V vi∈V,边 e i ∈ E e_i \in E ei∈E,单位矩阵 I n ∈ R n × n I_n \in \mathbb{R}^{n \times n} In∈Rn×n,邻接矩阵 A ∈ R n × n A \in \mathbb{R}^{n \times n} A∈Rn×n,关联矩阵 H ∈ R n × e H \in \mathbb{R}^{n \times e} H∈Rn×e,度矩阵 D ∈ R n × n D \in \mathbb{R}^{n \times n} D∈Rn×n,权重矩阵 W ∈ R n × n W \in \mathbb{R}^{n \times n} W∈Rn×n,拉普拉斯矩阵 L ∈ R n × n L \in \mathbb{R}^{n \times n} L∈Rn×n,特征值矩阵 Λ ∈ R n × n \Lambda \in \mathbb{R}^{n \times n} Λ∈Rn×n,特征向量矩阵 U ∈ R n × n U \in \mathbb{R}^{n \times n} U∈Rn×n。
- 连通图:对于一个无向图,如果任意节点 v i v_i vi 都能通过一些边到达节点 v j v_j vj,则称 G G G 为连通图
- 连通分量:无向图 G G G 的一个极大连通子图称为 G G G 的一个连通分量(或连通分支)。连通图只有一个连通分量(即其自身);非连通的无向图可能有多个连通分量
- 强连通图:给定有向图 G G G,并且给定 G G G 中的任意两个节点 u u u 和 v v v,如果节点 u u u 和 v v v 互相可达,则 G G G 称为强连通图。如下面有向图就是一个强连通图
- 弱连通图:若至少有一对节点不满足单向连通性,但去掉边的方向后从无向图的角度上看为连通图,则称为弱连通图。如下面弱连通图,节点 v 2 v_2 v2 在有向图角度上无法到达节点 v 1 v_1 v1,而在无向图的角度上,各个节点都互相可达
- 最短路径:两个节点之间的最短路径。如下面无向图 v 1 v_1 v1 到 v 5 v_5 v5 的最短路径为 2,即 v 1 → v 4 → v 5 v_1 \rightarrow v_4 \rightarrow v_5 v1→v4→v5 或 v 1 → v 2 → v 5 v_1 \rightarrow v_2 \rightarrow v_5 v1→v2→v5
- 图直径:图中所有所有节点之间最短路径中的最大值为图直径,如下面无向图的图直径为 2
- 度中心性:在无向图中,可以用一个节点的度来衡量中心性,其计算公式为 D C = N d e g r e e n − 1 DC = \frac{N_{degree}}{n-1} DC=n−1Ndegree。其中, N d e g r e e N_{degree} Ndegree 为节点的度(不带权的可简单理解为有几条边和该点相连),而 n n n 为节点总数量。如下面无向图中节点 v 3 v_3 v3 的度中心性为 D C = 2 5 − 1 = 0.5 DC = \frac{2}{5-1}=0.5 DC=5−12=0.5
- 特征向量中心性:求邻接矩阵 A A A 的特征值 λ i \lambda_i λi 和特征向量 x i \pmb{x}_i xi,最大特征值 λ m a x \lambda_{max} λmax 对应的特征向量 x m a x \pmb{x}_{max} xmax 即为特征向量中心性的值
- 中介中心性:图中经过某点 i i i 并且连接两点 s s s 和 t t t 之间的最短路径数目,占 s s s 和 t t t 之间最短路径总数的比,其公式如下:
B C = ∑ s , t ≠ i d s t ( i ) d s t BC = \sum_{s, t \ne i} \frac{d_{st}(i)}{d_{st}} BC=s,t=i∑dstdst(i)
✍️其中, d s t d_{st} dst 为 s s s 到 t t t 的最短路径数量,而 d s t ( i ) d_{st}(i) dst(i) 表示从 s s s 到 t t t 的最短路径中经过节点 i i i 的数量
- 连接中心性:(节点总数n - 1)/节点到其他节点最短路径之和。以下面无向图的节点 v 3 v_3 v3 为例, v 3 → v 1 v_3 \rightarrow v_1 v3→v1 的最短路径为 1, v 3 → v 2 v_3 \rightarrow v_2 v3→v2 的最短路径为 2, v 3 → v 4 v_3 \rightarrow v_4 v3→v4 的最短路径为 1, v 3 → v 5 v_3 \rightarrow v_5 v3→v5 的最短路径为 2,节点总数 n = 5 n=5 n=5,故连接中心性为 C C = 5 − 1 1 + 2 + 1 + 2 = 2 3 CC = \frac{5-1}{1+2+1+2} = \frac{2}{3} CC=1+2+1+25−1=32
- 邻接矩阵:节点
v
i
v_i
vi 和节点
v
j
v_j
vj 之间有边则将矩阵对应位置
(
i
,
j
)
(i, j)
(i,j) 置为
1
(不带权)或相应权重(带权)。以上面无向图为例,其不带权邻接矩阵如下:
A = ( v 1 v 2 v 3 v 4 v 5 v 1 0 1 1 1 0 v 2 1 0 0 0 1 v 3 1 0 0 1 0 v 4 1 0 1 0 1 v 5 0 1 0 1 0 ) A = \left( \begin{array}{c|cc} & v_1 & v_2 & v_3 & v_4 & v_5 \\ \hline v_1 & 0 & 1 & 1 & 1 & 0 \\ v_2 & 1 & 0 & 0 & 0 & 1 \\ v_3 & 1 & 0 & 0 & 1 & 0 \\ v_4 & 1 & 0 & 1 & 0 & 1 \\ v_5 & 0 & 1 & 0 & 1 & 0 \end{array}\right) A= v1v2v3v4v5v101110v210001v310010v410101v501010
- 关联矩阵:对于有向图,若节点
v
i
v_i
vi 在边
e
j
e_j
ej 的起点则将对应位置
(
i
,
j
)
(i, j)
(i,j) 置
1
,若在边的终点则置-1
,不在边上则置0
;对于无向图,若节点在边上则置1
,否则置0
。以上面弱连通图为例,其关联矩阵如下:
H = ( e 1 e 2 e 3 e 4 v 1 1 0 0 0 v 2 0 1 − 1 0 v 3 − 1 − 1 0 1 v 4 0 0 1 − 1 ) H = \left( \begin{array}{c|cc} & e_1 & e_2 & e_3 & e_4 \\ \hline v_1 & 1 & 0 & 0 & 0 \\ v_2 & 0 & 1 & -1 & 0 \\ v_3 & -1 & -1 & 0 & 1 \\ v_4 & 0 & 0 & 1 & -1 \\ \end{array}\right) H= v1v2v3v4e110−10e201−10e30−101e4001−1
- 度矩阵:该矩阵为对角矩阵,其值为连接到该节点所有边的权重之和(权重默认为 1);若为有向图,则该矩阵可分为出度矩阵和入度矩阵。以上面无向图为例,其度矩阵如下:
D = ( 3 0 0 0 0 0 2 0 0 0 0 0 2 0 0 0 0 0 3 0 0 0 0 0 2 ) D = \begin{pmatrix} 3 & 0 & 0 & 0 & 0 \\ 0 & 2 & 0 & 0 & 0 \\ 0 & 0 & 2 & 0 & 0 \\ 0 & 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & 0 & 2 \\ \end{pmatrix} D= 3000002000002000003000002
- 拉普拉斯矩阵:其值为对应度矩阵-邻接矩阵(有向图则需要选择出度或入度矩阵),即 L = D − A L = D - A L=D−A。以上面无向图为例,其拉普拉斯矩阵:
L = D − A = ( 3 0 0 0 0 0 2 0 0 0 0 0 2 0 0 0 0 0 3 0 0 0 0 0 2 ) − ( 0 1 1 1 0 1 0 0 0 1 1 0 0 1 0 1 0 1 0 1 0 1 0 1 0 ) = ( 3 − 1 − 1 − 1 0 − 1 2 0 0 − 1 − 1 0 2 − 1 0 − 1 0 − 1 3 − 1 0 − 1 0 − 1 2 ) L = D-A = \begin{pmatrix} 3 & 0 & 0 & 0 & 0 \\ 0 & 2 & 0 & 0 & 0 \\ 0 & 0 & 2 & 0 & 0 \\ 0 & 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & 0 & 2 \\ \end{pmatrix} - \begin{pmatrix} 0 & 1 & 1 & 1 & 0 \\ 1 & 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 & 1 \\ 0 & 1 & 0 & 1 & 0 \end{pmatrix} = \begin{pmatrix} 3 & -1 & -1 & -1 & 0 \\ -1 & 2 & 0 & 0 & -1 \\ -1 & 0 & 2 & -1 & 0 \\ -1 & 0 & -1 & 3 & -1 \\ 0 & -1 & 0 & -1 & 2 \end{pmatrix} L=D−A= 3000002000002000003000002 − 0111010001100101010101010 = 3−1−1−10−1200−1−102−10−10−13−10−10−12
- 特征值矩阵:特征值构成的对角矩阵
- 特征向量矩阵:特征向量构成的正交矩阵
GNN简介
🙀现实中越来越多的问题可以抽象成非欧氏结构数据,非欧氏数据中心节点的邻居节点数量和排列顺序不固定,不满足平移不变性,这就很难在非欧氏数据中定义卷积核。这导致现有的神经网络通常只能对常规的欧氏结构数据进行处理,传统的深度学习模型已经不能处理这种数据,从而亟需研究设计一种新的深度神经网络。而图神经网络(GNN)所处理的数据对象就是具有不规则结构的图数据,GNN 便在这种大背景下应运而生。图数据包含节点(实体)、边(关系)和整个图结构,GNN 的处理任务也主要从 Node-level
、Edge-level
和 Graph-level
这几个角度出发。
- Graph-level task:预测整个图的单一属性(图的标签)。例如对于用图表示的分子,我们可能想要预测分子闻起来像什么,或者它是否会与与疾病有关的受体结合;对于 MNIST 和 CIFAR 的图像分类问题,我们希望将标签与整个图像关联起来;对于文本,类似的问题是情绪分析,我们想要一次性确定整个句子的情绪
- Node-level task:预测整张图中每个节点的一些属性(节点的标签)。在图像中,节点级预测问题类似于实例分割,我们试图标记图像中每个像素的类别;对于文本,类似的任务是预测句子中每个单词的词性
- Edge-level task:预测边的一些属性(关系)以及存在与否(边的标签)。给定表示图像中对象的节点,我们希望预测这些节点中哪些共享一条边,或者该边的值是多少
😿图数据有节点、边、全局信息和连通性这四种信息,而使用神经网络解决图问题首先需要一个合适的方法对图的信息进行表示。对于节点、边和全局信息,可通过 Embedding
向量来表示,而连通性可通过最简单的邻接矩阵来表示。但使用邻接矩阵来表示连通性时,由于节点的顺序改变并不改变原始图,这将导致不同的邻接矩阵可以表示相同的图,即图和邻接矩阵不是一一对应的。此外,由于节点间并不总有关系,当图的节点有很多时,邻接矩阵可能会变得非常稀疏,这将导致计算效率很低且内存占用过高。一种更加优雅且节省内存的方法是采用邻接列表(adjacency lists),它将节点
n
i
n_i
ni 和
n
j
n_j
nj 之间的边
e
k
e_k
ek 的连通性描述为邻接列表第
k
k
k 项中的一个元组
(
i
,
j
)
(i, j)
(i,j)。如下图,第 1
条边连接节点
n
0
n_0
n0 和
n
1
n_1
n1,故邻接列表的第 1
项为
(
1
,
0
)
(1, 0)
(1,0)。
✍上图中节点、边和全局使用的是标量,但实际上更多使用的是向量形式。因此,需要处理的不是大小为 [ n n o d e s n_{nodes} nnodes] 的节点张量,而是大小为 [ n n o d e s n_{nodes} nnodes, n o d e d i m node_{dim} nodedim] 的节点张量。
GCN
🎉具体参考这篇文章,讲得挺细致的了,这里就不多赘述😴
PYG极简入门
😸PyG (PyTorch Geometric)是一个建立在 PyTorch 之上的库,其可用于编写和训练图神经网络。安装的 PYG 需要与 pytorch 和 CUDA 版本对应,否则可能会出现一些错误,具体参考官方安装文档。以 Pytorch 1.12.*
、CUDA 11.6
在 Windows
上以 Pip
方式安装,其命令如下:
pip install torch-scatter torch-sparse torch-cluster torch-spline-conv torch-geometric -f https://data.pyg.org/whl/torch-1.12.0+cu116.html
✍️之前安装 PYG 后出现 pytorch 的 CUDA 不可用,我这里直接重新安装对应版本的 pytorch 解决问题
Data Handling of Graphs
😸PyG 中的单个图是由 torch_geometry.data.Data
的一个实例所描述的,该实例默认包含以下属性:
- data.x:节点特征矩阵,其形状为
[num_nodes, num_node_features]
- data.edge_index:用于存储节点之间的边,其在 COO 格式下的形状为
[2, num_edges]
且数据类型为torch.long
- data.edge_attr:边特征矩阵,其形状为
[num_edges, num_edge_features]
- data.y:训练数据标签,若 node-level 的任务则其形状为
[num_nodes, *]
,若为 graph-level 的任务则其形状为[1, *]
- data.pos:节点位置矩阵,其形状为
[num_nodes, num_dimensions]
✍️实际上,Data 对象并不限制于这些属性中,可以通过 data.face
来扩展 Data
。
😴以下是一个通过 PYG 构建图数据的简单例子,其包含 5 个节点(索引从 0 开始),7 条边(边的两端定义在 edge_index 中):
import torch
from torch_geometric.data import Data
from torch_geometric.utils import to_networkx
import matplotlib.pyplot as plt
import networkx as nx
# 简易可视化函数
def visualize_graph(G, color):
plt.figure(figsize=(7, 7))
plt.xticks([])
plt.yticks([])
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=41), with_labels=True, node_color=color, cmap="Set2")
plt.show()
edge_index = torch.tensor([[0, 1, 1, 2, 2, 3, 4],
[1, 2, 4, 0, 3, 4, 2]], dtype=torch.long) # 第一行为边起点,第二行为边终点
x = torch.tensor([[0], [1], [2], [3], [4]], dtype=torch.float) # 节点特征,这里使用一维特征
data = Data(x=x, edge_index=edge_index) # 构建 Data 实例
G = to_networkx(data=data) # 将 Data 实例转换到 networkx.Graph
visualize_graph(G=G, color='pink') # 可视化构建的图
✍️当 edge_index
存储方式为 [num_edges, 2]
时,需要先对其进行转置操作再使用 contiguous()
方法
Common Benchmark Datasets
😻PYG 内置了一些通用的基准数据集(如 Cora、Citeseer、Pubmed),可通过 torch_geometric.datasets
来导入相关数据集。这里以 KarateClub
数据集为例,进行一些简易操作:
from torch_geometric.datasets import KarateClub
from torch_geometric.utils import to_networkx
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
# 简易可视化函数
def visualize_graph(G, color):
plt.figure(figsize=(7, 7))
plt.xticks([])
plt.yticks([])
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=42), with_labels=False, node_color=color, cmap="Set2")
plt.show()
dataset = KarateClub()
print(f'Dataset: {dataset}') # 数据集名称,KarateClub()
print('='*20)
print(f'Number of graphs: {len(dataset)}') # 图数量,1
print(f'Number of features: {dataset.num_features}') # 点特征数量,34
print(f'Number of classes: {dataset.num_classes}') # 数据类别数量,4
data = dataset[0] # 获取 dataset 中的一个数据(图)
print(f'An undirected graph: {data.is_undirected()}') # 是否无向图,True
print('='*30)
print(f'Num of edges: {data.num_edges}') # 边数量,156=78x2(因为无向图,所以乘2)
print(f'Num of nodes: {data.num_nodes}') # 点数量,34
print(f'Num of node features: {data.num_node_features}') # 点特征数量,34
print(f'Shape of x: {np.shape(data.x)}') # 节点特征矩阵,[34, 34]
print(f'Shape of edge_index: {np.shape(data.edge_index)}') # 边的起终点矩阵,[2, 156]
print(f'Attr of edge: {data.edge_attr}') # 边特征矩阵,None
print(f'Shape of y: {np.shape(data.y)}') # 真值,[34](对应 34 个点)
print(f'Pos of data: {data.pos}') # 节点位置矩阵,None
G = to_networkx(data, to_undirected=True) # 将 Data 实例转换到 networkx.Graph
visualize_graph(G, data.y) # 可视化构建的图
Mini-batches
😸神经网络通常以批处理的方式进行训练,而 PyG 通过创建稀疏的对角邻接矩阵(由 edge_index
定义)并在节点维度上连接特征(X)和目标矩阵(Y)来实现 mini-batch
的并行化,这种组合允许不同数量的节点和边使用在同一个批次中。此外,PYG 有其自身的 DataLoader(torch_geometric.loader.DataLoader
),这个 DataLoader 已经处理好了前面的拼接操作。
😻通过 PYG 的 DataLoader 可以很方便的使用 mini-batch,以下是一个数据加载的例子:
from torch_geometric.datasets import TUDataset
from torch_geometric.loader import DataLoader
from torch_scatter import scatter_mean
# 下载数据集,数据存放路径为当前路径下 data/ENZYMES 文件夹
dataset = TUDataset(root='data/ENZYMES', name='ENZYMES', use_node_attr=True)
# 加载数据集,使用的 batch size 为 32,且打乱顺序
loader = DataLoader(dataset, batch_size=32, shuffle=True)
# 获取每一个 batch
for batch in loader:
print(batch) # DataBatch(edge_index=[2, 4036], x=[1016, 21], y=[32], batch=[1016], ptr=[33])
print(batch.num_graphs) # 32
x = scatter_mean(batch.x, batch.batch, dim=0) # 分别为每个图的节点维度计算平均的节点特征
print(x.size()) # torch.Size([32, 21])
break # 迭代一次后退出
✍️torch_geometric.data.Batch
继承自 torch_geometric.data.Data
,并且多了一个 batch
属性,该属性是一个列向量,它将每个节点映射到每个 mini-batch 中的对应的图中,其形式如下:
b
a
t
c
h
=
[
0
⋯
0
1
⋯
n
−
2
n
−
1
⋯
n
−
1
]
T
batch = \begin{bmatrix}0 & \cdots & 0 & 1 & \cdots & n-2 & n-1 & \cdots & n-1 \end{bmatrix}^T
batch=[0⋯01⋯n−2n−1⋯n−1]T
Data Transforms
😺transforms
在计算机视觉领域是一种很常见的数据增强,而 PyG 有自己的 transforms
,其输出是 Data
类型,输出也是 Data
类型。可以使用 torch_geometric.transforms.Compose
封装一系列的 transforms
。我们以 ShapeNet 数据集 (包含 17000 个 point clouds,每个 point 分类为 16 个类别的其中一个)为例:
import torch_geometric.transforms as T
from torch_geometric.datasets import ShapeNet
# 下载并加载数据集
dataset = ShapeNet(root='data/ShapeNet', categories=['Airplane'],
pre_transform=T.KNNGraph(k=6), # 生成最近邻图
transform=T.RandomTranslate(0.01)) # 在一定范围内随机平移每个点
print(dataset[0]) # Data(x=[2518, 3], y=[2518], pos=[2518, 3], category=[1], edge_index=[2, 15108])
Learning Methods on Graphs
😴这里以一个简单的 GCN 模型构造和训练为例,该模型使用数据集为 Cora,加载数据集不使用 Dataset 和 DataLoader:
import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
from torch_geometric.datasets import Planetoid
# 下载并加载数据集
dataset = Planetoid(root='data/Planetoid', name='Cora')
# 定义 GCN 模型
class GCN(torch.nn.Module):
def __init__(self, hidden_channels):
super().__init__()
torch.manual_seed(12345)
self.conv1 = GCNConv(dataset.num_features, hidden_channels)
self.conv2 = GCNConv(hidden_channels, dataset.num_classes)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index)
x = F.dropout(F.relu(x), p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return x
# 模型相关配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = GCN(hidden_channels=16).to(device)
data = dataset[0].to(device)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
# 训练函数,返回损失值
def train():
model.train()
optimizer.zero_grad()
out = model(data)
loss = criterion(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
return loss
# 测试函数,返回准确率
def test():
model.eval()
out = model(data)
pred = out.argmax(dim=1)
test_correct = pred[data.test_mask] == data.y[data.test_mask]
test_acc = int(test_correct.sum()) / int(data.test_mask.sum())
return test_acc
# 训练 200 轮
for epoch in range(1, 201):
loss = train()
print(f'Train loss: {loss:.4f}') # Train loss: 0.0223
test_acc = test()
print(f'Test accuracy: {test_acc:.4f}') # Test accuracy: 0.8070
补充:A Gentle Introduction to Graph Neural Networks、图的一些基本知识:关联矩阵、拉普拉斯矩阵、图中心性【点度中心性】【 特征向量中心性】【中介中心性】【接近中心性】、图神经网络 PyTorch Geometric 入门教程