一、基本介绍
1. 图的基本定义
-
图由顶点(Vertex)以及连接顶点的边(Edge)构成。顶点表示研究的对象,边表示两个对象之间特定的关系。
-
图可以表示为顶点和边的集合,记为G=(V, E),其中V是顶点集合,E是边集合。
-
图又分为有向图和无向图;非加权图和加权图;连通图和非连通图(看是否有孤立的点)等。
2. torch_geometric中的数据存储方式
下面通过调用现有的数据集,查看数据集内部的存储结构。
# 从datasets中获取KarateClub数据集(和之前的内容有点像)
# 其实GNN是基于torch编写的,很多代码和之前类似
from torch_geometric.datasets import KarateClub
dataset = KarateClub()
print(f"Dataset:{dataset}:")
print("***********************")
print(f"Number of Graphs:{len(dataset)}") # 有多少个点?
print(f"Number of features:{dataset.num_features}") # 每个点的特征维度是多少?
print(f"Number of classes:{dataset.num_classes}") # 有多少类?
输出结果
由此看到这个数据集里只包含一张图。下面继续:
data = dataset[0] # 获取第0个元素(第一个图)
print(data)
输出结果
这个图一共有34个点,每个点由34维向量表示,有78条边,每个点都有标签。(如果有的点没有标签train_mask会小于34)
# 查看顶点特征
print(data.x)
输出结果
注意全是tensor向量。
edge_index = data.edge_index
print(edge_index.t())
输出结果
edge_index表示图的连接关系,这里并不是NN的表示,而是2N的矩阵,其中每个小矩阵的第一个元素表示起点,第二个元素表示终点,起点指向终点。
- edge_index:表示图的连接关系
- node_features:每个点的特征
- node_labels:每个点的标签
- train_mask:有的节点没有标签(用来表示哪些节点需要计算损失)
3. torch_geometric.utils的可视化工具:networkx
from torch.geometric.utils import to_networkx
import matplotlib.pyplot as plt
import networkx as nx
#现在要可视化刚刚那张有34个顶点,78条边的图
G = to_networkx(data, to_undirected=True) #表示无向图
plt.figure(figsize=(7,7))
plt.xticks([])
plt.xticks([])
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=42), with_labels=False, node_color=color, cmap='Set2')
plt.show()
输出结果如上图
4. Graph Neural Network网络定义
可以自行看官方文档
import torch
from torch.nn import Linear #线性层
from torch_geometric.nn import GCNConv #卷积层
这个过程非常简单,和之前定义MLP网络很像
class GCN(torch.nn.Module):
def __init__(self):
super().__init__()
torch,manual_seed(1234)
self.conv1 = GCNConv(dataset.num_features, 4)
self.conv2 = GCNConv(4,4)
self.conv3 = GCNConv(4,2) #最终是4类,但是这里输出2维向量,方便可视化
self.classifier = Linear(2, dataset.num_classes)
# 定义前向传播网络,我也不知道什么时候用tanh激活什么时候用relu
def forward(self, x, edge_index): #注意别忘记输入边的信息,相当于邻接矩阵
h = self.conv1(x,edge_index)
h = h.tanh()
h = self.conv2(h, edge_index)
h = h.tanh()
h = self.conv3(h, edge_index)
h = h.tanh()
# 分类层
out = self.classifier(h)
return out, h
model = GCN()
print(model) #打印model结构
输出结果
这里,我们首先可视化未训练之前的特征(epoch=0)
_, h = model(data.x, data.edge_index) #前面为什么加了“_”?
print(f'embedding_shape:{list(h.shape)}') #34个点,2维向量
h = h.detach().numpy()
plt.scatter(h[:,0], h[:,1], s=140, c=data.y,cmap='Set2')
if epoch is not None and loss is not None:
plt.xlabel(f"Epoch:{epoch}, loss:{loss.item():.4f}",fontsize=16)
plt.show()
输出结果
embedding_shape:[34, 2]
下面看看训练模型之后特征是什么样子的
import time
model = GCN()
critation = torch.nn.CrossEntropyLoss() #交叉熵损失
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) @Adam优化器
def train(data):
optimizer.zero_grad() #梯度清零
out, h = model(data.x, data.edge_index) #带分类层输出和不带分类层
loss = critation(out[data.train_mask], data.y[data.train_mask]) #半监督
loss.backward()
optimizer.step()
return loss, h
for epoch in range(401):
loss, h = train(data)
if epoch % 10 == 0:
visualize_embedding(h, color=data.y, epoch=epoch, loss=loss)
time.sleep(0.3)
二、节点分类任务
图嘛,节点和边可以生成好多个任务,比如给边推断节点类型,给点和边补全未知边,等等。现在,介绍一个最简单的节点分类任务。
目标:
对比MLP模型 和GCN模型的效果(在已有数据集上)
1. 数据集加载
查看数据基本信息
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
#下载数据集
dataset = Planetoid(root='data/Plantoid', name='Cora', transform=NormalizeFeatures())
print()
print(f'Dataset:{dataset}')
print("***********************************")
print(f'Number of fgraphs:{len(dataset)}')
print(f'Number of features:{dataset.num_features}')
print(f'Number of classes:{dataset.num_classes}')
data = dataset[0]
print()
print(data)
print('****************************************')
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'Number of training nodes:{data.train_mask.sum()}')
print(f'training node label rate:{int(data.train_mask.sum()) / data.num_nodes:.2f}')
输出结果
PS:这里不懂的小伙伴可以看第一章节对数据集存储结构的介绍部分↑
下面要先定义一个可视化函数,方便后面进行可视化
# TSNE可视化(降维)
%matplotlib inline
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
def visualize(h, color):
z = TSNE(n_components=2). fit_transform(h.detach().cpu().numpy())
plt.figure(figsize=(10,10))
plt.xticks([])
plt.yticks([])
plt.scatter(z[:, 0], z[:, 1], s=70, c=color, cmap='Set2')
plt.show
2. MLP模型
思考:传统的神经网络模型怎么对它进行分类?试着自己写一写代码
class MLP(torch.nn.Module):
def __init__(self, hidden_channels):
super().__init__()
torch.manual_seed(12345)
self.lin1 = Linear(dataset.num_features, hidden_channels)
self.lin2 = Linear(hidden_channels, dataset.num_classes)
def forward(self, x):
x = self.lin1(x)
x = x.relu()
x = F.dropout(x, p=0.5, training=self.training)
x = self.lin2(x)
return x
model = MLP(hidden_channels=16)
print(model)
输出网络结构
model = MLP(hidden_channels=16) #为什么是16我也不清楚
critation = 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.x)
loss = critation(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
return loss #返回的是训练损失
def test():
model.eval()
out = model(data.x)
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 #返回的是测试准确率
for epoch in range(1, 201):
loss = train()
print(f"Epoch:{epoch},Loss:{loss:.4f}")
输出训练损失
调用训练好的model,查看测试准确率
test_acc = test()
print(f"Test Accuracy:{test_acc:.4f}")
3. GCN模型
这里只需要把全连接层替换成GCN层
from torch_geometric.nn import GCNConv
class GCN(torch.nn.Module):
def __init__(self, hidden_channels):
super().__init__()
torch.manual_seed(1234567)
self.conv1 = GCNConv(dataset.num_features, hidden_channels)
self.conv2 = GCNConv(hidden_channels,dataset.num_classes)
# 为了公平,这里也用了两层,也用了relu激活函数
def forward(self, x, edge_index):
x = self.conv1(x,edge_index)
x = x.relu()
x = F.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return x
model = GCN(hidden_channels=16)
print(model)
输出网络结构
先可视化未训练的模型,由于输出是7维向量,所以降维成2维进行展示
model = GCN(hidden_channels=16)
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color = data.y)
训练模型
model = GCN(hidden_channels=16)
critation = 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.x, data.edge_index)
loss = critation(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
return loss
def test():
model.eval()
out = model(data.x, data.edge_index)
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
for epoch in range(1,101):
loss = train()
print(f"Epoch:{epoch},Loss:{loss:.4f}")
输出训练损失
计算测试准确率
test_acc = test()
print(f"Test Accuracy:{test_acc:.4f}")
训练后的可视化展示
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
疑问:为什么这里没有用线性层做全连接呢?
参考:
《深入浅出图神经网络》GNN原理解析
官方PyG的API文档