【模型学习之路】PyG的使用+基于点的任务
这一篇是关于PyG的基本使用
目录
前言
PyG的数据结构
演示
图的可视化
基于点的任务
任务分析
MLP
GCN
前言
对图结构感兴趣的朋友可以学一下常用的有关图结构的库:networkx详细介绍 `networkx` 库,探讨它的基本功能、如何创建图、操作图以及其常用参数。-CSDN博客
PyG零基础的朋友可以看一下这个视频的11~14集1-PyTorch Geometric工具包安装与配置方法_哔哩哔哩_bilibili
PyG的数据结构
演示
我们用一个简单的数据集作为演示。
import matplotlib.pyplot as plt
from torch_geometric.datasets import KarateClub
dataset = KarateClub() # 这是一个数据集,里面只有一个图
len(dataset)
# output
1
简单介绍一下每个数据的维度信息。
data = dataset[0]
print(data)
# output
Data(x=[34, 34], edge_index=[2, 156], y=[34], train_mask=[34])
-
x: 节点特征 [m, f] (m: 节点数,f: 特征数)。
-
edge_index: 边的索引 [2, e] (e: 边数)。可以看作是e条边,两两相连,然后转置了。
-
y: 标签 [m] (m: 节点数)。自然可以做成多输出的,那么维度就会是[m, n_tasks]
-
mask: [m] 一个相对玄学一点的东西,之后在不同场景中介绍
图的可视化
可以利用networks做可视化
import networkx as nx
from matplotlib import pyplot as plt
from torch_geometric.utils import to_networkx
from visualize import visualize_graph
def visualize_graph(G, color):
plt.figure(figsize=(5, 5))
plt.xticks([])
plt.yticks([])
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=42), node_color=color,
with_labels=False, node_size=100, cmap='Set2')
G = to_networkx(data)
visualize_graph(G, color=data.y)
在PyG中,邻接矩阵edge_index是按照稀疏矩阵的方式存的,我们可以把转化为我们平时之前用的密集矩阵。
# 转化为密集型的邻接矩阵
G_adj = nx.to_numpy_array(G)
print(G_adj)
# output
[[0. 1. 1. ... 1. 0. 0.]
[1. 0. 1. ... 0. 0. 0.]
[1. 1. 0. ... 0. 1. 0.]
...
[1. 0. 0. ... 0. 1. 1.]
[0. 0. 1. ... 1. 0. 1.]
[0. 0. 0. ... 1. 1. 0.]]
顺便画个热力图
import seaborn as sns
sns.heatmap(G_adj, cmap='Blues')
顺便把邻接矩阵和度矩阵都画一下
import numpy as np
adj = G_adj
d = np.diag(np.sum(adj, axis=1))
adj = adj + np.eye(adj.shape[0])
d = d + np.eye(adj.shape[0])
plt.figure(figsize=(10, 4))
plt.subplot(121)
sns.heatmap(adj, cmap='Blues')
plt.subplot(122)
sns.heatmap(d, cmap='Blues')
plt.show()
基于点的任务
任务分析
先像前言中教程一样,先拿到数据集,然后做一些分析。
import torch
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
dataset = Planetoid(root='data/Planetoid', name='Cora', transform=NormalizeFeatures())
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')
#output
Number of graphs: 1
Number of features: 1433
Number of classes: 7
# 就一张图,直接取出来便是
data = dataset[0]
print(data)
print(f'Number of nodes: {data.num_nodes}')
print(f'Number of edges: {data.num_edges}')
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}')
print(f'Training node label ratio: {data.train_mask.sum().item() / data.num_nodes:.2f}')
print(f'Has isolated nodes: {data.has_isolated_nodes()}')
print(f'Has self-loops: {data.has_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')
#output
Data(x=[2708, 1433], edge_index=[2, 10556], y=[2708], train_mask=[2708], val_mask=[2708], test_mask=[2708])
Number of nodes: 2708
Number of edges: 10556
Average node degree: 3.90
Training node label ratio: 0.05
Has isolated nodes: False
Has self-loops: False
Is undirected: True
上面的很多指标意思都很明显,就不过多解释了。
重点是,基于点的任务到底要干什么?
基于点的任务,往往会约束在同一张图中。这张图有很多节点,如果它们有标签值,就可以划分到训练集(train_mask为True的点)、验证集(valid_mask为True的点)和测试集(test_mask为True的点),用于模型的训练与预测。
我们的目标是,对于一些没有标签值的点,我们就用训练好的模型去预测它们。
所以,其实这个任务非常加了邻接矩阵的MLP。我们将两者对比一下。
MLP
模型定义很简单,这是一个7分类问题。
import torch
import torch.nn as nn
class MLP(torch.nn.Module):
def __init__(self):
super(MLP, self).__init__()
torch.manual_seed(666)
self.fc = nn.Sequential(
nn.Linear(1433, 256),
nn.ReLU(),
nn.Linear(256, 64),
nn.ReLU(),
nn.Linear(64, 7)
)
def forward(self, x):
return self.fc(x)
训练的时候,注意一下细节:
1. 这个数据集用train_mask和test_mask来划分训练集和测试集。在training时,只用在train_mask为True的上面计算损失即可。同样在计算testing的acc时,也只需要用到test_mask为True的即可。
2. x的shape为[2708, 1433],这里整个训练采用的是最传统的方式,即整个数据集不划分batch(或者说整个数据集就是一个batch)
model = MLP()
criterion = 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.x)
loss = criterion(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
return loss.item()
def test():
model.eval()
out = model(data.x)
pred = out.argmax(dim=1) # [2708, 7] -> [2708]
acc = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item() / data.test_mask.sum().item()
return acc
mlp_loss_lst = []
mlp_acc_lst = []
for epoch in range(1, 201):
"""
input: [2708, 1433]
output: [2708, 7]
"""
loss = train()
acc = test()
mlp_loss_lst.append(loss)
mlp_acc_lst.append(acc)
GCN
先来看看模型。
PyG将图神经网络里面各种经典的架构基本都有实现,这里的GCNConv直接调库,原理的话我们上一个专栏详细说过了:【模型学习之路】手写+分析GAT_手写gat-CSDN博客
仔细观察,GCNConv在维度上的表现简直就跟nn.Linear一模一样!
当然,内部自然要复杂的多。此外,它在调用时还要有edge_index,格式和前面的data.edge_index是统一的。
import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
class GCN(torch.nn.Module):
def __init__(self):
super(GCN, self).__init__()
torch.manual_seed(666)
self.conv1 = GCNConv(1433, 16)
self.conv2 = GCNConv(16, 7)
def forward(self, x, edge_index):
x = self.conv1(x, edge_index) # [2708, 1433] -> [2708, 16]
x = F.relu(x)
x = F.dropout(x, training=self.training)
x = self.conv2(x, edge_index) # [2708, 16] -> [2708, 7]
return x
训练过程大差不差。
model = GCN()
criterion = 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.x, data.edge_index)
loss = criterion(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
return loss.item()
def test():
model.eval()
out = model(data.x, data.edge_index)
pred = out.argmax(dim=1) # [2708, 7] -> [2708]
acc = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item() / data.test_mask.sum().item()
return acc
gcn_loss_lst = []
gcn_acc_lst = []
for epoch in range(1, 201):
"""
input: [2708, 1433]
output: [2708, 7]
"""
loss = train()
acc = test()
gcn_loss_lst.append(loss)
gcn_acc_lst.append(acc)
两者的对比也是挺明显的:
plt.figure(figsize=(10, 5))
plt.plot(mlp_acc_lst, label='MLP')
plt.plot(gcn_acc_lst, label='GCN')
plt.ylim(0, 1)
plt.legend()
plt.show()