SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS

基于图卷积网络的半监督分类

文献参考

引言(INTRODUCTION)

我们考虑在图(如引文网络)中对节点(如文档)进行分类的问题,其中只有一小部分节点有标签。这一问题可以被框架化为基于图的半监督学习,其中标签信息通过某种形式的显式图正则化(如使用图拉普拉斯正则化项)在图上进行平滑。然而,这种假设可能会限制模型的容量,因为图边不一定编码节点相似性,还可能包含其他信息。

在本工作中,我们使用神经网络模型直接编码图结构,并对所有有标签的节点进行监督目标训练,从而避免在损失函数中显式的图正则化。通过条件化神经网络模型于图的邻接矩阵,我们的模型能够从监督损失中分布梯度信息,并能够学习有标签和无标签节点的表示。

我们的贡献有两方面。

首先,我们介绍了一种简单且行为良好的逐层传播规则,用于直接在图上操作的神经网络模型,并展示了其可以从频谱图卷积的一阶近似中得到动机。

其次,我们展示了这种图神经网络模型可以用于快速且可扩展的图中节点的半监督分类。在多个数据集上的实验表明,我们的模型在分类准确率和效率上都优于现有的半监督学习方法。

快速近似图卷积(FAST APPROXIMATE CONVOLUTIONS ON GRAPHS)

在本节中,我们为特定的基于图的神经网络模型 提供理论动机,并将在本文其余部分中使用该模型。我们考虑一个多层图卷积网络(GCN),其逐层传播规则如下:

这里, 是无向图 的邻接矩阵,并添加了自环。 是单位矩阵, 是第 层特定的可训练权重矩阵。 表示激活函数,例如 ReLU: 是第 层的激活矩阵,

在下文中,我们将展示这种传播规则的形式可以通过局部化的频谱滤波器在图上的一阶近似来动机化(Hammond et al., 2011; Defferrard et al., 2016)。

频谱图卷积(Spectral Graph Convolutions)

我们考虑图上的频谱卷积定义为在傅里叶域中信号与滤波器的乘积,即:

其中 是归一化图拉普拉斯矩阵 的特征向量矩阵。直接计算这种形式的卷积是昂贵的,因为与特征向量矩阵 相乘的复杂度是 。为了规避这一问题,Hammond 等人提出可以通过切比雪夫多项式的截断展开来近似:

其中 的最大特征值。切比雪夫多项式 递归定义为

层级线性模型(Layer-Wise Linear Model)

基于这种近似方法,我们可以堆叠多个卷积层,每层后接一个逐点非线性激活函数。假设我们将卷积操作限制为 ,即卷积函数是 的线性函数,并假设,则公式简化为:

为了进一步稳定数值计算,引入重新归一化技巧:

并将其推广到信号

gs

gs2

gs3

gs4

半监督节点分类(SEMI-SUPERVISED NODE CLASSIFICATION)

示例(Example)

在一个对称邻接矩阵 的图上进行节点分类时,我们首先计算 ,然后我们的前向模型形式如下:

其中, 是可训练的权重矩阵,softmax 激活函数用于输出层。

实现(Implementation)

参考PyG文档,GCNConv可以进行学习查看,GCN参数可以进行查看,公式结构可以进行查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

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
# 避免重复 softmax:
#在使用 CrossEntropyLoss 时,输出层不需要显式添加 softmax,因为损失函数内部已经包含了 log_softmax 操作。
model = GCN(hidden_channels=16)
print(model)

1
2
3
4
GCN(
(conv1): GCNConv(1433, 16)
(conv2): GCNConv(16, 7)
)

使用Cora数据集(借助)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from torch_geometric.datasets import Planetoid #下载数据集用的
from torch_geometric.transforms import NormalizeFeatures

dataset = Planetoid(root='data/Planetoid', name='Cora', transform=NormalizeFeatures())#transform预处理

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'Has isolated nodes: {data.has_isolated_nodes()}')
print(f'Has self-loops: {data.has_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')

结果显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dataset: Cora():
======================
Number of graphs: 1
Number of features: 1433
Number of classes: 7

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
Number of training nodes: 140
Training node label rate: 0.05
Has isolated nodes: False
Has self-loops: False
Is undirected: True
Done!

可视化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 可视化部分
%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()

该函数将高维节点特征嵌入降维到二维,并使用不同的颜色表示不同类别的节点。

散点图可以直观地展示节点特征的分布和聚类情况,有助于分析和理解图神经网络的性能和效果。

1
2
3
4
5
model = GCN(hidden_channels=16)
model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

t1

模型训练验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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()
out = model(data.x, data.edge_index)
loss = criterion(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, 201):
loss = train()
print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

#Test Accuracy: 0.8020

结果显示

1
2
3
4
model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

t2