图卷积神经网络(GCN)综述与实现(PyTorch版)
本文的实验环境为PyTorch = 1.11.0 + cu113,PyG = 2.0.4
,相关依赖库和数据集的下载请见链接。
一、图卷积神经网络介绍
1.1 传统图像卷积
卷积神经网络中的卷积(Convolution)指的是在图像上进行的输入和卷积核之间离散内积运算,其本质上就是利用共享参数的滤波器,通过计算中心值以及相邻节点的值进行加权获得带有局部空间特征的特征提取器。
其具有三个重要的特征,分别为:
- 稀疏连接
- 相较于全连接层,卷积层输入和输出间的连接是稀疏的,能够大大减少参数的数量,加快网络的训练速度。
- 参数共享
- 卷积核的权重参数可以被多个函数或操作共享,这样只需要训练一个参数集,而不需要对每个位置都训练一个参数集。此外,由于卷积核的大小一般是小于输入大小,也能起到减少参数数量的作用
- 等变表示
- 事实上,每个卷积层可以通过多个卷积核来进行特征提取,并且在卷积运算后,卷积神经网络对输入的图像具有平移不变性(有严格的数学论证)
- 事实上,每个卷积层可以通过多个卷积核来进行特征提取,并且在卷积运算后,卷积神经网络对输入的图像具有平移不变性(有严格的数学论证)
一般在卷积层后,会通过一个池化层进行降维,进一步降低网络的复杂度和非线性程度。在那之后,可以将通过卷积池化层后的特征输入全连接层或反卷积层进行进一步的分类、分割、重建工作。当然,传统的卷积操作一般适用于结构化数据。
1.2 图结构
图作为一种典型的非结构化非线性数据(非欧几里得数据),其可以表示一对一、一对多、多对多的关系,因而常被用于描述复杂的数据对象,譬如社交网络、知识图谱、城市路网、3D点云等。与结构化数据不同,图的局部输入维度可变,即每个节点的邻居节点数量不同;图具有无序性,即节点间并不存在先后关系,仅存在连接关系(点云是置换不变性,在无序性的基础上,交换两点或多点不会影响整体结果)。由于图结构的特殊性,传统CNN和RNN对其的表征能力并不理想。
对于图结构,我们可以将其抽象表示为:
在这里表示图中节点的集合,而为边的集合。对于图特征,我们一般有三个重要矩阵进行表示。
- 邻接矩阵:
adjacency matrix
用来表示节点间的连接关系。
对于带权的图,邻接矩阵将把1替换为对应的权重
-
度矩阵:
degree matrix
用来表示节点的连接数,可以表征某个节点在图中的重要程度,是一个对角矩阵,例如针对上图的入度矩阵:
-
特征矩阵:
feature matrix
用来表示节点的特征
1.3 图卷积神经网络
目前主流的图卷积基本上可以分为两类,一种是基于谱的图卷积,一种是基于空域的图卷积。
基于谱的图卷积通过傅里叶变换(FFT干的一件事情就是连接空域和频域)将节点映射到频域空间,通过频域空间上的卷积来实现时域上的卷积,最后将特征映射回空域。而基于空域的图卷积则是直接基于节点与邻居进行卷积提取特征,没有做域上的变换。
图卷积算子可表示为:
其中,设中心结点为;为结点在层的特征表达;是非线性激活函数;则是归一化因子,譬如结点度的倒数、反距离权重、高斯衰减权重等;是结点的邻接节点(包括自身);表示节点的类型;表示类型的节点变换权重参数。
下面围绕Semi-supervised Classification with Graph Convolutional Networks一文中提出的GCN结构进行分析。
该篇文章由阿姆斯特丹(the University of Amsterdam)大学机器学习专业的Thomas Kipf 博士于2016年提出,并于2017年被深度学习的顶会ICLR(International Conference on Learning Representations)接收!这位大佬的研究方向是学习结构化数据和结构化表示/计算,包括推理、(多智能体)强化学习和结构化深度生成模型。
核心思想
该篇文章提出了一种新的网络结构,用于处理非结构化的图数据,并解决了在一个图中,只有少部分节点的标签是已知情况下的节点分类问题(半监督学习)。
对于带有特征的图结构,例如下图中的网络结构,有部分是带有标识的,而有部分则是无标识的。
GCN通过考虑节点本身以及邻居节点的特征信息来提取潜在的关系。比如我们在中心节点拼接了邻居的特征后,使用平均池化的方式对这些特征进行聚合,再通过浅层网络进行学习训练得到新的数据。
每个GCN层做的事情:
- 获取当前节点和邻接节点特征
- 通过聚合函数获取局部特征(带有拓扑关系)
- 浅层学习训练,获取高维特征
数学推导
对于一个如上图所示的无向图,我们要怎么样才能获取到某个节点以及其邻居节点的特征呢?一种非常直观的想法💡是,在当前节点的特征之后,根据权重拼接与之相邻节点的特征。此时,空域上的距离往往成为了权重的影响因素。
那么,邻接表就成了我们考虑节点间拓扑关系最重要的结构。我们写成,是邻接矩阵,是特征矩阵,是权重。右乘相当于控制行,左乘相当于控制列,左乘,相当于在对应节点处,使用哪些节点特征构建新的特征:
诶,糟糕,这矩阵数数数着数着咋把自己给忘了呀!而且似乎…有些小朋友发育的过于良好,这样会导致梯度消化不良的!(梯度爆炸或消失问题)
为了处理这种情况,我们引入了新的邻接矩阵用于承载当前节点信息~
当权重因子取时,,此时意味着节点中心和邻居一样重要啦。🎉
值得注意的是,本身也是一个可以由训练得到的参数。
针对梯度问题呢,我们可以利用对角矩阵实现标准化,譬如利用度矩阵的逆进行平均化的操作:
于是乎,新的公式诞生啦:
但我们在这里仅仅对行进行了标准化,在列方向上并没有被标准化,所以,修正后的公式为:
当然了,行做了一次标准化,列做了一次标准化,那么对于元素来说,那就是做了两次标准化诶!我们再开个方吧:happy:
于是乎,最终可以得到以下的标准格式:
是可训练的权重,是标准化后的邻接矩阵。
此时我们在回过头来看这个图卷积算子,是不是有些恍然大悟了呢🤔
Thomas经过实验比对,浅层的GCN(一般为2层)取得的效果往往要比没有加入残差块的深层GCN来的好。GCN的网络层数代表着节点特征所能到达的最远距离,或者说节点信息的传播距离。深层的网络可能会令节点信息传播到整个网络,反而效果不那么好。
举个简单的栗子~🐈
假设有邻接矩阵:
对于节点来说,跟他接轨的只有节点,那么在第一层计算后,节点聚合了来自节点的特征。而对于节点,与之接轨的则有节点,经过第一层后,该节点聚合了所有节点的信息。那么在第二层,由于图结构不是动态更新的,又会聚合的特征信息,但此时的已经聚合了的特征信息!换言之,感受到了来自远方的召唤!(的信息经过两层网络后传递到了处,传递过程类似于并查集啦)
最后对于这些分类任务,采用交叉熵函数作为损失函数,计算获取最可能的情况即可。
二、基于PyG的图卷积神经网络半监督分类
2.1 模型准备
模块导入阶段,在本实验中,networkx
主要用于图结构可视化,argparse
用于参数的读写
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
from torch_geometric.datasets import Planetoid
from torch_geometric.utils import to_networkx
import networkx as nx
from sklearn.metrics import accuracy_score
from sklearn.manifold import TSNE
from sklearn.svm import SVC
from sklearn.semi_supervised import _label_propagation
import argparse
2.2 数据探查
我们导入Cora数据集。
Cora数据集由2078篇机器学习领域的论文构成,每个样本点都是一篇论文,这些论文主要分为了7个类别,分别为基于案例、遗传算法、神经网络、概率方法、强化学习、规则学习与理论。在该数据集中,每篇论文都至少引用了该数据集中的另一篇论文,对每个节点所代表的论文,都由一个1433维的词向量表示,即该图上每个节点都具有1433个特征,词向量的每个元素都对应一个词,且该元素仅有0或1两个取值,取0表示该元素对应的词不在论文中,取1表示在。
Cora参数
- ind.cora.x : 训练集节点特征向量,保存对象为:scipy.sparse.csr.csr_matrix,实际展开后大小为: (140, 1433)
- ind.cora.tx : 测试集节点特征向量,保存对象为:scipy.sparse.csr.csr_matrix,实际展开后大小为: (1000, 1433)
- ind.cora.allx : 包含有标签和无标签的训练节点特征向量,保存对象为:scipy.sparse.csr.csr_matrix,实际展开后大小为:(1708, 1433),可以理解为除测试集以外的其他节点特征集合,训练集是它的子集
- ind.cora.y : one-hot表示的训练节点的标签,保存对象为:numpy.ndarray
- ind.cora.ty : one-hot表示的测试节点的标签,保存对象为:numpy.ndarray
- ind.cora.ally : one-hot表示的ind.cora.allx对应的标签,保存对象为:numpy.ndarray
- ind.cora.graph : 保存节点之间边的信息,保存格式为:{ index : [ index_of_neighbor_nodes ] }
- ind.cora.test.index : 保存测试集节点的索引,保存对象为:List,用于后面的归纳学习设置。
通过以下语句下载或读取,若当前路径下没有找到文件,则会自动下载。
# 导入Cora数据集
dataset=Planetoid(root=r"./Cora",name="Cora") # root: 指定路径 name: 数据集名称
# 查看数据的基本情况
print("网络数据包含的类数量:",dataset.num_classes)
print("网络数据边的特征数量:",dataset.num_edge_features)
print("网络数据边的数量:",dataset.data.edge_index.shape[1]/2) # 除以2是OOC的组织形式
print("网络数据节点的特征数量:",dataset.num_node_features)
print("网络数据节点的数量:",dataset.data.x.shape[0])
输出的结果为:
网络数据包含的类数量: 7
网络数据边的特征数量: 0
网络数据边的数量: 5278.0
网络数据节点的特征数量: 1433
网络数据节点的数量: 2708
网络的边的数量: 5278
网络的节点的数量: 2708
我们可以看到,Cora数据集囊括了七个类共2708个节点,每个节点都带有1433个特征,一共有5278条边,边没有包含特征。
重点来看dataset.data
:
Data(x=[2708, 1433], edge_index=[2, 10556], y=[2708], train_mask=[2708], val_mask=[2708], test_mask=[2708])
该数据中的x是节点和对应的特征矩阵,y是标签值,edge_index则是COO的网络连接格式(第一行表示节点顺序,第二行表示连接对象,因而每条边会出现两次),mask对象则是一个逻辑向量(与节点数量相同),表示该节点的用途。
2.3 结构可视化
为了更直观地分析数据,我们可以考虑将图结构可视化,但使用张量格式的数据十分不方便,PyG提供了to_networkx()
函数,可用于将torch_geometric.data.Data
对象转化为networkx
库中的有向图数据,方便进一步的分析和可视化。
# 通过networkx库进行可视化
CoraNet=to_networkx(dataset.data) # type : networkx.classes.graph.Graph
CoraNet=CoraNet.to_undirected() # 转化为无向图
# 查看网络情况
print("网络的边的数量: ",CoraNet.number_of_edges())
print("网络的节点的数量: ",CoraNet.number_of_nodes())
Node_class=dataset.data.y.data.numpy()
print("节点分类:",Node_class)
结果为:
网络的边的数量: 5278
网络的节点的数量: 2708
节点分类: [3 4 4 ... 3 3 3]
此时数据已经由Tensor
格式转化为Graph
格式啦,并且我们将标签y
单独提取出来,方便接下来的操作。
在Graph
类中,我们可以使用Graph.degree
方法计算度,度越大的节点,说明跟其他节点的连通性越好,在整个网络中具有更大的贡献度,即越重要。下面我们对前三十的节点进行可视化:
# 计算节点的度
Node_degree=pd.DataFrame(data=CoraNet.degree,columns=["Node","Degree"])
Node_degree=Node_degree.sort_values(by=["Degree"],ascending=False)
Node_degree=Node_degree.reset_index(drop=True)
# 使用直方图可视化度较多的前三十个节点
Node_degree.iloc[0:30,:].plot(x="Node",y="Degree",kind="bar",figsize=(10,7))
plt.xlabel("Node",size=12)
plt.ylabel("Degree",size=12)
plt.show()
文章出处登录后可见!