一、引言
结点表征的生成是图结点预测和边预测任务成功的关键。
基于图神经网络的结点表征学习可以理解为对图神经网络进行基于监督学习的训练,使得图神经网络学会产生高质量的结点表征。
在结点预测任务中,一个图,图上有很多节点,部分节点的标签已知,剩余节点的标签未知。将节点的属性(x)、边的端点信息(edge_index)、边的属性(edge_attr)输入到多层图神经网络,经过图神经网络每一层的一次结点间信息传递,图神经网络为结点生成结点表征。
任务为:根据结点的属性(可以是类别型、也可以是数值型)、边的信息、边的属性、已知的结点预测标签,对未知标签的结点做预测。
具体举例:以Cora数据集为例进行说明,Cora是一个论文引用网络,结点代表论文,如果两篇论文存在引用关系,那么认为对应的两个结点之间存在边,每个结点由一个1433维的词包特征向量描述。任务是推断每个文档的类别(共7类)。
通过结点分类任务来比较MLP和GCN, GAT三者的节点表征学习能力。
二、准备工作
获取并分析数据集
实现代码如下:
from torch_geometric.datasetsimport Planetoid
from torch_geometric.transformsimport NormalizeFeatures
dataset=Planetoid(root='data/Planetoid',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()}')
运行结果如下:
根据运行结果可以得出:Cora图拥有2,708个结点和10,556条边,平均结点度为3.9,共140个有真实标签的节点(每类20个)用于训练,有标签的结点的比例占5%。进一步可以看到,这个图是无向图,不存在孤立的节、结点(即每个文档至少有一个引文)。
数据转换在将数据输入到神经网络之前修改数据,这一功能可用于实现数据规范化或数据增强,使用NormalizeFeatures()方法。
可视化结点表征分布的方法
为了实现结点表征分布的可视化,我们先利用TANE将高维结点表征嵌入到二维平面空间,然后在二维平面空间画出节点。
import matplotlib.pyplotas plt
from sklearn.manifoldimport TSNE
def visualize(h, color):
z = TSNE(n_components=2).fit_transform(h.output.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()
三、MLP在图结点分类中的应用
构建一个简单的MLP,该网络只对输入结点的特征进行操作,在所有节点之间共享权重。
MLP图结点分类器:
import torch
from torch.nnimport Linear
import torch.nn.functionalas F
from torch_geometric.datasetsimport Planetoid
from torch_geometric.transformsimport NormalizeFeatures
dataset=Planetoid(root='data/Planetoid',name='Cora',transform=NormalizeFeatures())
data = dataset[0]
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)。
利用交叉熵损失和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)
def train():
model.train()
optimizer.zero_grad()# Clear gradients.
out =model(data.x)# 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 epochin range(1,201):
loss = train()
print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')
运行结果如下:
MLP测试
测试这个MLP神经网络在测试集上的表现
MLP只有大约59%的测试准确性。不准确的一个重要原因是,用于训练此神经网络的有标签结点数量过少,此神经网络被过拟合,对未见过的节点泛化性很差。
四、GCN及其在图节点分类任务中的应用
GCN的定义
GCN神经网络的数学的定义为:
其中表示插入自环的邻接矩阵,表示其对角线度矩阵。邻接矩阵可以包括不为1的值,当邻接矩阵不为{0,1}值时,表示邻接矩阵存储的是边的权重。
为对称归一化矩阵。
PyG中GCNConv模块说明
GCNConv构造函数接口:
GCNConv(in_channels: int, out_channels: int, improved: bool = False, cached: bool = False, add_self_loops: bool = True, normalize: bool = True, bias: bool = True, **kwargs)
in_channels:输入数据维度;
out_channels:输出数据维度;
improved:如果为true,其目的在于增强中心节点自身信息;
cached:是否存储的计算结果以便后续使用,这个参数只应在归纳学习的场景中设置为true;
add_self_loops:是否在邻接矩阵中增加自环边;
normalize:是否添加自环边并在运行中计算对称归一化系数;
bias:是否包含偏置项。
基于GCN图神经网络的图节点分类
通过将torch.nn.Linear layers 替换为PyG的GNN Conv Layers,可以将MLP模型转化为GNN模型。
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)
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
运行结果如下:
根据运行结果可以得出:7维特征的结点被嵌入到2维的平面上,存在同类结点聚集的情况。
训练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}')
运行结果如下:
训练过程结束后,检测GCN结点分类器在测试集上的准确性
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}')
运行结果如下:
通过将线性层替换成GCN层,测试准确率可以达到81.4%,高于MLP分类器。表明结点的邻接信息在取得更好的准确率方面起着关键作用。
可视化训练过的GCN模型
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
运行结果如下:
五、GAT及其在图节点分类任务中的应用
GAT的定义
图注意网络的数学定义为:
注意力系数的计算方法为:
PyG中GATConv 模块说明
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使用多少个注意力模型;
concat:如为true,不同注意力模型得到的结点表征被拼接到一起(表征维度翻倍),否则对不同注意力模型得到的结点表征求均值。
基于GAT图神经网络的图结点分类
将MLP例子中的linear层替换为GATConv层,来实现基于GAT的图结点分类神经网络。
import torch
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
model = GAT(hidden_channels=16)
print(model)
运行结果如下:
可视化未训练的GAT网络的结点表征
model = GAT(hidden_channels=16)
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
运行结果如下:
训练GAT结点分类器
model = GAT(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}')
运行结果如下:
训练过程结束后,检测GAT结点分类器在测试集上的准确性
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}')
运行结果如下:
通过将线性层替换成GATConv层,测试准确率可以达到73.8%,高于MLP分类器。
可视化训练过的GAT模型
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
运行结果如下:
六、MLP、GCN、GAT结点分类器的对比
在结点表征的学习中,MLP节点分类器只考虑了结点自身属性,忽略了结点之间的连接关系,它的结果是最差的;而GCN与GAT节点分类器,同时考虑了结点自身属性与周围邻居结点的属性,它们的结果优于MLP节点分类器。
从中可以看出邻居结点的信息对于结点分类任务的重要性。
基于图神经网络的结点表征的学习遵循消息传递范式:
在邻居结点信息变换阶段,GCN与GAT都对邻居结点做归一化和线性变换;
在邻居结点信息聚合阶段都将变换后的邻居结点信息做求和聚合;
在中心结点信息变换阶段只是简单返回邻居结点信息聚合阶段的聚合结果。
GCN与GAT的区别在于邻居结点信息聚合过程中的归一化方法不同:
1. GCN根据中心结点与邻居结点的度计算归一化系数,后者根据中心结点与邻居结点的相似度计算归一化系数。
2. GCN的归一化方式依赖于图的拓扑结构,不同结点其自身的度不同、其邻居的度也不同,在一些应用中可能会影响泛化能力。GAT的归一化方式依赖于中心结点与邻居结点的相似度,相似度是训练得到的,不受图的拓扑结构的影响,在不同的任务中都会有较好的泛化表现。
DataWhale开源学习资料:
https://github.com/datawhalechina/team-learning-nlp/tree/master/GNN