我们使用图神经网络来生成节点表征,并通过基于监督学习的对图神经网络的训练,使得图神经网络学会产生高质量的节点表征。高质量的节点表征能够用于衡量节点的相似性,同时高质量的节点表征也是准确分类节点的前提。
在节点预测任务中,我们拥有一个图,图上有很多节点,部分节点的标签已知,剩余节点的标签未知。将节点的属性(x)、边的端点信息(edge_index)、边的属性(edge_attr,如果有的话)输入到多层图神经网络,经过图神经网络每一层的一次节点间信息传递,图神经网络为节点生成节点表征。其大致任务是根据节点的属性(类别型或者是数值型)、边的信息、边的属性、已知的节点预测标签,对未知标签的节点做预测。
本节是以多层图神经网络为例,学习训练图神经网络的一般过程。我们将以Cora
数据集为例子进行说明,Cora
是一个论文引用网络,节点代表论文,如果两篇论文存在引用关系,则对应的两个节点之间存在边,各节点的属性都是一个1433维的词包特征向量。我们的任务是预测各篇论文的类别(共7类)。我们还将对MLP和GCN, GAT(两个知名度很高的图神经网络)三类神经网络在节点分类任务中的表现进行比较分析,以此来展现图神经网络的强大和论证图神经网络强于普通深度神经网络的原因。
内容安排如下:
- 获取并分析数据集、构建一个方法用于分析节点表征的分布。
- 考察MLP神经网络用于节点分类的表现,观察基于MLP神经网络学习到的节点表征的分布。
- 介绍GCN, GAT这两个图神经网络的理论、对比它们在节点分类任务中的表现以及它们学习到的节点表征的质量。
- 比较三者在节点表征学习能力上的差异。
一、准备工作
1. 获取并分析数据集
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures
dataset = Planetoid(root='dataset', name='Cora', transform=NormalizeFeatures())
print()
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] # Get the first graph object.
print()
print(data)
print('======================')
# Gather some statistics about the graph.
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}')
print(f'Contains isolated nodes: {
data.contains_isolated_nodes()}')
print(f'Contains self-loops: {
data.contains_self_loops()}')
print(f'Is undirected: {
data.is_undirected()}')
二、使用MLP神经网络进行节点分类
理论上,我们应该能够仅根据文章的内容,即它的词包特征表征(bag-of-words feature representation)来推断文章的类别,而无需考虑文章之间的任何关系信息。接下来,让我们通过构建一个简单的MLP神经网络来验证这一点。此神经网络只对输入节点的表征做变换,它在所有节点之间共享权重。
1. MLP神经网络的构造
import torch
from torch.nn import Linear
import torch.nn.functional as F
class MLP(torch.nn.Module):
def __init__(self, hidden_channels):
super(MLP, self).__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)
我们的MLP由两个线性层、一个ReLU
非线性层和一个dropout
操作组成。第一个线性层将1433维的节点表征嵌入(embedding)到低维空间中(hidden_channels=16
),第二个线性层将节点表征嵌入到类别空间中(num_classes=7
)。
2. MLP神经网络的训练与测试
我们利用交叉熵损失和Adam优化器来训练这个接单的MLP神经网络
model = MLP(hidden_channels=16)
criterion = torch.nn.CrossEntropyLoss() # Define loss criterion.
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) # Define optimizer.
def train():
model.train()
optimizer.zero_grad() # Clear gradients.
out = model(data.x) # Perform a single forward pass.
#这块有点不懂,为什么model里面是全部数据,但是在计算损失的时候只计算训练集里面的损失呢?也就是前向计算是全部数据,但是反向损失计算是训练集的损失,这是为什么呢?
loss = criterion(out[data.train_mask], data.y[data.train_mask]) # Compute the loss solely based on the training nodes.
loss.backward() # Derive gradients.
optimizer.step() # Update parameters based on gradients.
return loss
for epoch in range(1, 201):
loss = train()
print(f'Epoch: {
epoch:03d}, Loss: {
loss:.4f}')
#MLP神经网络的测试
def test():
model.eval()
out = model(data.x)
pred = out.argmax(dim=1) # Use the class with highest probability.
test_correct = pred[data.test_mask] == data.y[data.test_mask] # Check against ground-truth labels.
test_acc = int(test_correct.sum()) / int(data.test_mask.sum()) # Derive ratio of correct predictions.
return test_acc
test_acc = test()
print(f'Test Accuracy: {
test_acc:.4f}')
#Test Accuracy: 0.5740
MLP大约57%的测试准确性。因此可知是标签节点过少,此神经网络过拟合,缺乏泛化能力。
三、卷积图神经网络(GCN)
1. GCN的定义
GCN 来源于论文“Semi-supervised Classification with Graph Convolutional Network”,其数学定义为,
X ′ = D ^ − 1 / 2 A ^ D ^ − 1 / 2 X Θ , \mathbf{X}^{\prime} = \mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}} \mathbf{\hat{D}}^{-1/2} \mathbf{X} \mathbf{\Theta}, X′=D^−1/2A^D^−1/2XΘ,
其中 A ^ = A + I \mathbf{\hat{A}} = \mathbf{A} + \mathbf{I} A^=A+I表示插入自环的邻接矩阵(使得每一个节点都有一条边连接到自身), D ^ i i = ∑ j = 0 A ^ i j \hat{D}_{ii} = \sum_{j=0} \hat{A}_{ij} D^ii=∑j=0A^ij表示 A ^ \mathbf{\hat{A}} A^的对角线度矩阵(对角线元素为对应节点的度,其余元素为0)。邻接矩阵可以包括不为 1 1 1的值,当邻接矩阵不为{0,1}
值时,表示邻接矩阵存储的是边的权重。 D ^ − 1 / 2 A ^ D ^ − 1 / 2 \mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}} \mathbf{\hat{D}}^{-1/2} D^−1/2A^D^−1/2是对称归一化矩阵,它的节点式表述为:
x i ′ = Θ ∑ j ∈ N ( v ) ∪ { i } e j , i d ^ j d ^ i x j \mathbf{x}^{\prime}_i = \mathbf{\Theta} \sum_{j \in \mathcal{N}(v) \cup \{ i \}} \frac{e_{j,i}}{\sqrt{\hat{d}_j \hat{d}_i}} \mathbf{x}_j xi′=Θj∈N(v)∪{
i}∑d^jd^iej,ixj
其中, d ^ i = 1 + ∑ j ∈ N ( i ) e j , i \hat{d}_i = 1 + \sum_{j \in \mathcal{N}(i)} e_{j,i} d^i=1+∑j∈N(i)ej,i, e j , i e_{j,i} ej,i表示从源节点 j j j到目标节点 i i i的边的对称归一化系数(默认值为1.0)。
参考链接:https://github.com/datawhalechina/team-learning-nlp/tree/master/GNN
2. GCN图神经网络的构造
将上面例子中的torch.nn.Linear
替换成torch_geometric.nn.GCNConv
,我们就可以得到一个GCN图神经网络,如下方代码所示:
from torch_geometric.nn import GCNConv
class GCN(torch.nn.Module):
def __init__(self, hidden_channels):
super(GCN, self).__init__()
torch.manual_seed(12345)
self.conv1 = GCNConv(dataset.num_features, hidden_channels)
self.conv2 = GCNConv(hidden_channels, dataset.num_classes)
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)
GCN图神经网络的训练与测试
model = GCN(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()
def train():
model.train()
optimizer.zero_grad() # Clear gradients.
out = model(data.x, data.edge_index) # Perform a single forward pass.
loss = criterion(out[data.train_mask], data.y[data.train_mask]) # Compute the loss solely based on the training nodes.
loss.backward() # Derive gradients.
optimizer.step() # Update parameters based on gradients.
return loss
for epoch in range(1, 201):
loss = train()
print(f'Epoch: {
epoch:03d}, Loss: {
loss:.4f}')
def test():
model.eval()
out = model(data.x, data.edge_index)
pred = out.argmax(dim=1) # Use the class with highest probability.
test_correct = pred[data.test_mask] == data.y[data.test_mask] # Check against ground-truth labels.
test_acc = int(test_correct.sum()) / int(data.test_mask.sum()) # Derive ratio of correct predictions.
return test_acc
test_acc = test()
print(f'Test Accuracy: {
test_acc:.4f}')
在上面这个案例中将torch.nn.Linear
替换成torch_geometric.nn.GCNConv
,我们可以取得81.4%的测试准确率。这表明节点的邻接信息在取得更好的准确率方面起着关键作用。
下面分别对训练前和训练后的数据进行可视化对比:
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()
model.eval()#不启用 Batch Normalization 和 Dropout。
out = model(data.x, data.edge_index)
visualize(data.x, color=data.y)#对训练前的数据进行可视化
visualize(out, color=data.y)#对训练后的数据进行可视化
由可视化训练后的GCN图神经网络生成的节点表征,会发现“同类节点群聚”的现象更加明显了。这意味着在训练后,GCN图神经网络生成的节点表征质量更高了。
四、图注意力神经网络(GAT)
1.GAT的定义
图注意神经网络(GAT)来源于论文 Graph Attention Networks。其数学定义为,
x i ′ = α i , i Θ x i + ∑ j ∈ N ( i ) α i , j Θ x j , \mathbf{x}^{\prime}_i = \alpha_{i,i}\mathbf{\Theta}\mathbf{x}_{i} + \sum_{j \in \mathcal{N}(i)} \alpha_{i,j}\mathbf{\Theta}\mathbf{x}_{j}, xi′=αi,iΘxi+j∈N(i)∑αi,jΘxj,
其中注意力系数 α i , j \alpha_{i,j} αi,j的计算方法为,
α i , j = exp ( L e a k y R e L U ( a ⊤ [ Θ x i ∥ Θ x j ] ) ) ∑ k ∈ N ( i ) ∪ { i } exp ( L e a k y R e L U ( a ⊤ [ Θ x i ∥ Θ x k ] ) ) . \alpha_{i,j} = \frac{ \exp\left(\mathrm{LeakyReLU}\left(\mathbf{a}^{\top} [\mathbf{\Theta}\mathbf{x}_i \, \Vert \, \mathbf{\Theta}\mathbf{x}_j] \right)\right)} {\sum_{k \in \mathcal{N}(i) \cup \{ i \}} \exp\left(\mathrm{LeakyReLU}\left(\mathbf{a}^{\top} [\mathbf{\Theta}\mathbf{x}_i \, \Vert \, \mathbf{\Theta}\mathbf{x}_k] \right)\right)}. αi,j=∑k∈N(i)∪{
i}exp(LeakyReLU(a⊤[Θxi∥Θxk]))exp(LeakyReLU(a⊤[Θxi∥Θxj])).
GATConv
构造函数接口:
GATConv(in_channels: Union[int, Tuple[int, int]], out_channels: int, heads: int = 1, concat: bool = True, negative_slope: float = 0.2, dropout: float = 0.0, add_self_loops: bool = True, bias: bool = True, **kwargs)
其中:
in_channels
:输入数据维度;out_channels
:输出数据维度;heads
:在GATConv
使用多少个注意力模型(Number of multi-head-attentions);concat
:如为true
,不同注意力模型得到的节点表征被拼接到一起(表征维度翻倍),否则对不同注意力模型得到的节点表征求均值;
详细内容请大家参阅GATConv官方文档
2. GAT图神经网络的构造
将MLP神经网络例子中的torch.nn.Linear
替换成torch_geometric.nn.GATConv
,来实现GAT图神经网络的构造,如下方代码所示:
import torch
from torch.nn import Linear
import torch.nn.functional as F
from torch_geometric.nn import GATConv
class GAT(torch.nn.Module):
def __init__(self, hidden_channels):
super(GAT, self).__init__()
torch.manual_seed(12345)
self.conv1 = GATConv(dataset.num_features, hidden_channels)
self.conv2 = GATConv(hidden_channels, dataset.num_classes)
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
总结:
1.节点表征的学习过程中,MLP神经网络只考虑了节点自身属性,忽略了节点之间的连接关系,他的结果最差。
2.GCN与GAT考虑了节点自身信息和周围邻接节点的信息,因此结果都优于MLP。
3.GCN图神经网络与GAT图神经网络的相同点为:
- 它们都遵循消息传递范式;
- 在邻接节点信息变换阶段,它们都对邻接节点做归一化和线性变换;
- 在邻接节点信息聚合阶段,它们都将变换后的邻接节点信息做求和聚合;
- 在中心节点信息变换阶段,它们都只是简单返回邻接节点信息聚合阶段的聚合结果。
4.GCN图神经网络与GAT图神经网络的区别在于采取的归一化方法不同:
- 前者根据中心节点与邻接节点的度计算归一化系数,后者根据中心节点与邻接节点的相似度计算归一化系数。
- 前者的归一化方式依赖于图的拓扑结构:不同的节点会有不同的度,同时不同节点的邻接节点的度也不同,于是在一些应用中GCN图神经网络会表现出较差的泛化能力。
- 后者的归一化方式依赖于中心节点与邻接节点的相似度,相似度是训练得到的,因此不受图的拓扑结构的影响,在不同的任务中都会有较好的泛化表现。
作业
- 使用PyG中不同的图卷积模块在PyG的不同数据集上实现节点分类或回归任务。
参考文献
- PyG中内置的数据转换方法:torch-geometric-transforms
- 一个可视化高纬数据的工具:t-distributed Stochastic Neighbor Embedding
- 提出GCN的论文:Semi-supervised Classification with Graph Convolutional Network
- GCNConv官方文档:torch_geometric.nn.conv.GCNConv
- 提出GAT的论文: Graph Attention Networks