摘要
本周我深入研读了一篇题为《Semi-Supervised Classification with Graph Convolutional Networks》的文献。该文介绍了一种名为图卷积网络(Graph Convolutional Networks, GCNs)的网络结构。GCNs是传统卷积算法在图结构数据上的变体,直接适用于处理图数据。阅读文献后,我进一步深入学习了图神经网络(GNN)的相关知识,并通过实例代码成功实现了一个点分类任务。这次学习让我更好地理解了GCN在半监督分类中的应用,以及如何借助GNN处理复杂的图结构数据。
ABSTRACT
This week, I delved into a paper titled "Semi-Supervised Classification with Graph Convolutional Networks." The paper introduces a network architecture known as Graph Convolutional Networks (GCNs). GCNs are a variant of traditional convolutional algorithms designed specifically for handling graph-structured data. After reading the literature, I furthered my understanding of Graph Neural Networks (GNNs) and successfully implemented a node classification task using example code. This learning experience enhanced my comprehension of GCN's application in semi-supervised classification and how GNNs can effectively process intricate graph-structured data. The practical implementation of code not only reinforced theoretical knowledge but also laid a solid foundation for future applications in a broader context.
一、文献阅读
1、题目
题目:Semi-Supervised Classification with Graph Convolutional Networks
期刊/会议:ICLR
链接:https://arxiv.org/abs/1609.02907
2、摘要
本文提出了一种图卷积网络(graph covolutional networks, GCNs),该网络是传统卷积算法在图结构数据上的一个变体,可以直接用于处理图结构数据。从本质上讲,GCN 是谱图卷积(spectral graph convolution) 的局部一阶近似(localized first-order approximation)。GCN的另一个特点在于其模型规模会随图中边的数量的增长而线性增长。总的来说,其可用于具有图结构数据的半监督学习。
This article introduces a type of Graph Convolutional Network (GCN), which is a variant of traditional convolutional algorithms designed for graph-structured data and can be directly applied to process such data. Essentially, GCN is a localized first-order approximation of spectral graph convolution. Another characteristic of GCN is that its model scale linearly increases with the growth of the number of edges in the graph. In summary, GCN can be employed for semi-supervised learning tasks with data structured as graphs.
3、基础标识
1、(Adjacency Matrix)表示图的邻接矩阵。表示顶点之间相邻关系的矩阵。
的计算方式为A + I,表示带自环的邻接矩阵。
2、(Degree Matrix)表示图的度矩阵。为对角矩阵,对角上的元素为各个顶点的度。顶点vi的度表示和该顶点相关联的边的数量。(无向图中一般记录的是单纯的入度或者出度)。
3、(Laplacian Matrix)表示图的拉普拉斯矩阵。其为半正定矩阵,运算规则是D-A。其中
为节点
的度。
4、(Symmetric Normalized Laplacian Matrix)表示对称归一化的拉普拉斯矩阵。表示为L左乘D的-1/2次方,再右乘D的-1/2次方;也可以表示为用单位矩阵I减去A左乘D的-1/2次方,再右乘D的-1/2次方。式子如下:
此矩阵的另一种计算方式:
4、文献解读
1、谱图卷积
从本质上说,GCN是谱图卷积的一阶局部近似。那么,什么是谱图卷积?
首先来看图上的谱卷积。图上的谱卷积可以定义为信号与滤波器
在傅里叶域的乘积:
其中,为归一化拉普拉斯
的特征向量矩阵(即谱矩阵),其中,
为相应的特征值矩阵(对角矩阵),
为
的图傅氏变换。在这里,可以将
看作是
特征向量的函数,即
。
对于图谱卷积来说,其计算代价无疑是很大的:1) 使用进行矩阵乘法运算的计算复杂度为
;(2)计算大图的拉普拉斯矩阵
的特征分解需要很大的计算量。针对这一问题,本文采用了[2]中的方法来近似
。该方法使用切比雪夫多项式(Chebyshevpolynomial)的
的K阶截断来获得对
的近似:
其中,为经
的最大特征值(即谱半径)缩放后的特征向量矩阵。
表示一个切比雪夫向量,切比雪夫多项式使用递归的方式进行定义:
,其中,
且
。
此时,可以使用近似的替代原来的
,此时,可以得到:
而是
的
阶多项式,且有
,其中,
。这样,我们就得到了文中的公式:
通过这一近似,可以发现,谱图卷积不再依赖于整个图,而只是依赖于距离中心节点 步之内的节点(即
阶邻居)。在[3]中,Defferrard et al. 使用了这一概念定义了图上的卷积神经网络。
2、Layer-wise线性模型
近似的谱图卷积虽然可以建立起阶邻居的依赖,然而,却仍然需要在
上进行
阶运算。在实际过程中,这一运算的代价也是非常大的。为了降低运算代价,本文进一步简化了该运算,即限定
=1 。此时,谱图卷积可以近似为
(或
)的线性函数。
当然,这样做的代价是,只能建立起一阶邻居的依赖。对于这一问题,可以通过堆积多层图卷积网络建立阶邻居的依赖,而且,这样做的另一个优势是,在建立
>1 阶邻居的依赖时,不需要受到切比雪夫多项式的限制。
为了进一步简化运算,在GCN的线性模型中,本文定义。此时,我们可以得到图谱卷积的一阶线性近似:
可以看到,该式中仅有两个参数与
。若需建立
阶邻居上的依赖,可以通过设置
层这样的滤波器来实现。
在实际的过程中,可以通过对参数进行约束来避免过拟合,并进一步简化运算复杂度。例如,可以令,从而得到
需要注意的是, 的特征值范围为[0,2],这意味着,当不停地重复该操作时(网络非常深时),可能会引起梯度爆炸或梯度消失。为了减弱这一问题,本文提出了一种 renormalization trick:
其中,,
=
。
当图中每个节点的表示不是单独的标量而是一个大小为的向量时,可以使用其变体进行处理:
其中,表示参数矩阵,
为相应的卷积结果。此时,每个节点的节点表示被更新成了一个新的
维向量,该
维向量包含了相应的一阶邻居上的信息。
3、图卷积神经网络
经过以上的推导,本文得到了图卷积神经网络的(单层)最终形式:
其中, 第层网络的输入为
(初始输入为
),
为图中的节点数量,每个节点使用
维的特征向量进行表示。
为添加了自连接的邻接矩阵,
为度矩阵,
=
。
为待训练的参数。
为相应的激活函数,例如
。此即GCN的最终形式。
4、Model
对于一个大图(例如“文献引用网络”),我们有时需要对其上的节点进行分类。然而,在该图上,仅有少量的节点是有标注的。此时,我们需要依靠这些已标注的节点来对那些没有标注过的节点进行分类,此即半监督节点分类问题。在这类问题中,由于大部分节点都没有已标注的标签,因此往往需要使用某种形式的图正则项对标签信息进行平滑(例如在损失函数中引入图拉普拉斯正则(graph Laplacian regularization)):
其中,表示有监督的损失,
可以是一个类似于神经网络的可微函数。
表示一个权值因子,
则是相应的节点向量表示。
表示未归一化的图拉普拉斯矩阵。这种处理方式的一个基本假设是:相连的节点可能有相同的标签。然而,这种假设却往往会限制模型的表示能力,因为图中的边不仅仅可以用于编码节点相似度,而且还包含有额外的信息。
GCN的使用可以有效地避开这一问题。GCN通过一个简单的映射函数,可以将节点的局部信息汇聚到该节点中,然后仅使用那些有标注的节点计算
即可,从而无需使用图拉普拉斯正则。
具体来说,本文使用了一个两层的GCN进行节点分类。模型结构图如下图所示:
其具体流程为:
首先获取节点的特征表示并计算邻接矩阵
。
将其输入到一个两层的GCN网络中,得到每个标签的预测结果:
其中,为第一层的权值矩阵,用于将节点的特征表示映射为相应的隐层状态。
为第二层的权值矩阵,用于将节点的隐层表示映射为相应的输出(
对应节点标签的数量)。最后将每个节点的表示通过一个softmax函数,即可得到每个标签的预测结果。
对于半监督分类问题,使用所有有标签节点上的期望交叉熵作为损失函数:
其中, 表示有标签的节点集。
5、Experiments
针对半监督节点分类问题,本文主要进行了两个实验:一是在文献引用网络上的实验,二是在知识图谱上的实验(NELL)。在文献引用网络中,边使用引用链构建,节点表示相应的文档。本文共使用了三个引用网络数据集:Citeseer、Cora与Pubmed。其数据统计的结果如下表所示(Label rate表示有标注节点的比例):
在这些数据集上的实验结果如下表所示:
可以看到,相比于其他模型,基于GCN的半监督节点分类模型的效果有了较大的提升。
6、总结
本文提出了一种图卷积神经网络,该网络可以被有效地用于处理图结构的数据。图卷积神经网络具有几个特点:
局部特性:图卷积神经网络关注的是图中以某节点为中心,K阶邻居之内的信息,这一点与GNN有本质的区别;
一阶特性:经过多种近似之后,GCN变成了一个一阶模型。也就是说,单层的GCN可以被用于处理图中一阶邻居上的信息;若要处理K阶邻居,可以采用多层GCN来实现;
参数共享:对于每个节点,其上的滤波器参数
是共享的,这也是其被称作图卷积网络的原因之一。
二、GNN网络结构
下面使用消息传递神经网络来构建GNN。 GNN采用“图入图出”架构,也就是说模型将图作为输入,改变图的信息后进行输出。
1、最简单的GNN层
GNN的一层:对顶点向量embedding、边向量embedding和全局向量embedding分别构造一个多层感知机(MLP),这三个MLP就共同组成了一个GNN的层,输入是embedding,输出也是embedding,embedding的值进行了更新,但维度不变。
顶点embedding,边embedding和全局embedding的输入-处理(MLP)-输出是分开进行的,因为有3个MLP,分别各自处理顶点、边或者全局信息。
代表全局信息,
代表所有的顶点embedding,
代表所有的边embedding,每个f(带有下标)就是一个多层感知机,共同组成了GNN的一层。
2、对图信息进行预测
当一个图的embedding经过了多层GNN层的处理后,输出的embedding中可能就含有了大量丰富的信息,将这些embedding通过若干全连接层处理(或者其他分类器)就可以对顶点进行预测了。
但是可能存在一些问题,例如,有一些顶点中信息缺失(embedding无法表征顶点的特点),但是和它相连的边以及全局含有有用的信息,我们就需要将边和全局的信息传递给对应的顶点。
方法:将与该顶点相连接的边的embedding与全局embedding求和(或者其他方式),得到的结果加到顶点embedding中(如果维度不同需要做映射),如下图所示。这个方法我们称为Pooling。
这样经过池化Pooling以后每个顶点中都含有丰富信息,对于边信息缺失或者全局信息缺失我们采用对称的方法进行处理。
可以将最简单的GNN模型总结成如下的结构。
一张图输入,经过GNN层(实质上就是三个分别对应点、边和全局的MLP),输出一个属性已经变换的图,在经过全连接层,得到输出。
3、对GNN层进行优化
我们从(2)对图信息进行预测得到启发,考虑是否可以在每一层GNN层中加入Pooling,让相邻顶点、边、以及全局信息之间进行信息传递,而不是等到最后进行预测的时候才进行Pooling。那么,在每一次Pooling(也就是消息传递)操作中我们就利用了顶点之间的连接信息,因为在进行消息传递的时候需要知道那些顶点是相邻的,每个顶点有哪些边。
信息传递(或者说Pooling)包括三个步骤:
a. 对于图中的每个顶点,收集所有相邻顶点或边的embedding。
b. 通过聚合函数(如求和)聚合所有收集到的embedding。
c. 合并的embedding通过一个更新函数传递,通常是一个学习过的神经网络。
通过将消息传递 GNN 层堆叠在一起,一个节点最终可以整合来自整个图的信息:在三层之后,一个节点拥有离它三步之遥的节点的信息。
将加入了消息传递 的GNN 层堆叠在一起,一个顶点最终可以整合来自整个图的信息:在三层之后,一个顶点拥有离它三步距离的顶点的信息。我们将优化后的GNN模型用图来表示
为了让GNN网络能更好的对图进行预测,提取出图中更加丰富的特征信息,我们不局限于在相邻顶点之间进行消息传递,顶点和边之间,边和边之间,顶点和全局信息之间,以及边和全局信息之间都可以进行消息传递。
之前的消息传递,只考虑了邻接的顶点和边,如果一个顶点想要聚合距离它很远的顶点和边的信息呢?
为此提出master node(一个虚拟的顶点,它和图中所有的顶点和边虚拟地连接),这个顶点的embedding就是全局信息U。
在顶点信息传递给边的时候,也会把U一起传递,把边信息传递给顶点的时候,也会把U一起传递;然后更新边和顶点后,将边和顶点的信息一起汇聚给U,之后做MLP更新。
三、使用GCN完成点分类任务
一、数据集描述
使用
Planetoid
类下载了名为 "Cora" 的图数据集,同时应用了NormalizeFeatures
进行特征的规范化处理。打印了数据集的基本信息,包括数据集的名称、图的数量、节点特征的数量、类别的数量。
获取了数据集中的第一个图对象
data
。打印了关于该图的一些统计信息,如节点数量、边的数量、平均节点度等。
打印了训练节点的数量、训练节点标签的比例,以及一些图的结构特性,如是否有孤立节点、是否有自环、是否是无向图。
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. Dataset信息:
数据集名称是 Cora。
数据集中包含了1个图
每个节点具有1433个特征
数据集共有7个类别。
2. Graph信息:
该图共有2708个节点
包含10556条边
平均节点度为3.90。3.训练集信息:
训练集中有140个节点
训练节点的标签比例为0.05 (5%)
二、节点可视化设置
%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()
三、使用全连接层进行实验
这段代码实现了一个简单的多层感知机(MLP)模型。该模型包括两个线性层,通过 ReLU 激活函数和 dropout 操作,用于学习输入特征并进行分类。具体而言,模型的第一层接收数据集的节点特征,第二层输出预测的类别分数。这个 MLP 模型是为了在图数据上执行节点分类任务而设计的,其中隐藏层维度为16。
import torch
from torch.nn import Linear
import torch.nn.functional as F
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)
四、模型训练
这段代码实现了一个在图数据上进行节点分类的训练过程。首先,定义了一个包含两个线性层的多层感知机(MLP)模型,其中使用ReLU激活函数和dropout操作进行非线性映射和防止过拟合。通过Adam优化器和交叉熵损失函数进行模型训练,其中加入了L2正则化项以防止过度拟合。训练过程中,模型经过多轮迭代,通过计算损失函数梯度来更新参数,以最小化损失。在每个训练周期结束后,输出当前训练损失。最终目标是通过这个训练过程,使得模型能够准确地对图中的节点进行分类。
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.
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
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
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}')
结果如下:
我们发现用全连接层来进行点分类正确率只有59%,下面我们换成GCN来测试一下。
六、 将全连接层替换成GCN层
GCN类具有两个GCNConv层,
conv1
和conv2
,在初始化时指定了隐藏通道的数量。forward
方法定义了GCN的前向传播,其中输入特征x
和边索引edge_index
通过GCNConv层,并在它们之间使用了ReLU激活和dropout。使用16个隐藏通道实例化了模型,并打印了模型的结构。
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
model = GCN(hidden_channels=16)
print(model)
结果表明:这个GCN模型被设计用于处理1433个输入特征(可能是节点的特征),并在两个隐藏层中输出16和7个通道的特征。这样的结构通常用于图数据的节点分类问题,其中模型的目标是将每个节点分到7个不同的类别中。
七、 降维成2维进行展示
model = GCN(hidden_channels=16)
model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)
八、训练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()
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, 101):
loss = train()
print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')
九、准确率计算
test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')
从59%到81%,这个提升还是蛮大的;训练后的可视化展示如下:
可以发现一类的点大多数都被分在了一块,说明GCN的表现是比全连接网络要好很多的。
总结
本周我深入学习了《Semi-Supervised Classification with Graph Convolutional Networks》,了解了图卷积网络(GCNs)及其在半监督分类中的应用。通过实际代码实现点分类任务,我更深刻地理解了GCNs在处理图结构数据方面的优势。下周我计划继续探索图神经网络领域,关注更多复杂任务的实际应用。