关键字:图神经网络(Graph Neural Network, GNN), 图卷积神经网络(Graph Convolutional Network, GCN)
本文适合:对图神经网络不了解的人,快速了解图神经网络的输入输出和计算过程. 本文讲的很浅,但可以让读者快速了解基本的图卷积网络的输入输出。
需要的背景知识:
- 数据结构中图的概念;
- 线性代数中的矩阵、矢量与矩阵乘法;
- 编程中数组、稀疏矩阵的概念
- 一点点神经网络基础
1 图神经网络中的图是什么样的图?
首先,不是图片,而是数据结构中的图结构。它包含结点和边。
其次,图上有图信号,什么是图信号?
你可以理解为,每个节点上都有一个长度为F的矢量。这个矢量也可被称为节点的特征。每个节点的特征,也就是这个矢量,长度是一样的,我们在这里假设这个矢量的长度都为F。
2 图卷积需要什么样的输入?
实际上,图卷积的输入包含两部分:
- 邻接矩阵
- 每个节点的图信号
那么具体是什么样的数据结构来输入呢?比如邻接矩阵,是用二维数组,还是用三元组呢?这取决于你的模型对输入的具体要求了。
在实际操作中,往往没有图信号,比如我们输入一个人际交往的矩阵,节点代表人,而边代表这两个人是否认识,但我们并没有一个矢量作为特征来刻画一个人。这种情况下往往使用单位阵来表示图信号。如:对于N个节点,用N*N单位阵表示图信号,其中每一行是一个节点上的图信号。
3 图卷积的计算过程,输出是什么?
很多人会说,图卷积有两种理解:空域和频域。但实际上,它操作起来没那么复杂。本文不讲那么深,直接上它的公式:
其中:
N : 节点个数
Input_dim : 每个节点的特征(图信号)的维度
output_dim : 模型输出的维度
Adjacency(A) : 邻接矩阵
Feature(F) : 所有节点的特征构成的矩阵,每一行是一个节点的特征
Weight(W) : 权重矩阵,在模型训练中不断更新该矩阵
从公式中我们可以看出来,最终的输出形状是N*Output_dim,对于每个节点,都有了一个维度为Output_dim的表示。
我们可以把整个图卷积看成两个过程:
- FW 是对属性信息进行仿射变换,用来学习属性特征
- AFW 从空域聚合邻居节点,学习节点的结构信息
后面可以再接具体的任务,比如节点分类,分7类,那就再接一个7个节点的全连接层输出,输出的结果和label计算损失函数,然后反向传播就好。
4 现成的代码
代码用pytorch实现,需要一定的pytorch基础。
具体代码,包含数据的读取,处理,见
https://github.com/FighterLYL/GraphNeuralNetwork/tree/master/chapter5
下面我们讲其中一些重要的部分:
图卷积层:
class GraphConvolution(nn.Module):
def __init__(self, input_dim, output_dim, use_bias=True):
super(GraphConvolution, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.use_bias = use_bias
self.weight = nn.Parameter(torch.Tensor(input_dim, output_dim))
if self.use_bias:
self.bias = nn.Parameter(torch.Tensor(output_dim))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
nn.init.kaiming_uniform_(self.weight)
if self.use_bias:
nn.init.zeros_(self.bias)
def forward(self, adjacency, input_feature):
"""
:param adjacency:
:param input_feature:
:return:
"""
support = torch.mm(input_feature, self.weight)
output = torch.sparse.mm(adjacency, support)
if self.use_bias:
output += self.bias
return output
我们可以看到,卷积层就是走的上面提到的公式,但是这个只是一层卷积层,用的时候,要把这层网络加到模型里:
class GcnNet(nn.Module):
"""
一个包含两层GraphConvolution的模型
"""
def __init__(self, input_dim=1433, hidden_dim=16, output_dim=7):
"""
:param input_dim: 输入结点的矢量,1433维
:param hidden_dim: 隐藏层节点个数
:param output_dim: 输出维度,因为我们最终是把论文分为7类,所以是7
"""
super(GcnNet, self).__init__()
self.gcn1 = GraphConvolution(input_dim, hidden_dim,)
self.gcn2 = GraphConvolution(hidden_dim, output_dim)
def forward(self, adjacency, feature):
h = F.relu(self.gcn1(adjacency, feature))
logits = self.gcn2(adjacency, h)
return logits
网络写好了就可以用了,用的时候要注意,与普通的MLP、CNN等不同,GCN要输入两个东西,一个是邻接矩阵,一个是每个节点的特征,所以,要这样用:
logits = model(tensor_adjacency, tensor_x)
其中logits是模型输出,tensor_adjacency, tensor_x分别是邻接矩阵和节点特征。另外要注意,一般来说,邻接矩阵在输入模型前需要用如下方式normalization一下(相当于左乘了归一化的拉普拉斯矩阵,不懂的话直接用就好)
def normalization(adjacency):
"""计算L=(D^-0.5) * (A+I) * (D^-0.5)"""
adjacency += sp.eye(adjacency.shape[0])
degree = np.array(adjacency.sum(1)) # 横向求和,算度数
d_hat = sp.diags(np.power(degree, -0.5).flatten()) # 把 degree开-0.5次方,然后变成一个对角阵
return d_hat.dot(adjacency).dot(d_hat).tocoo()
完整的代码是分类的任务。输入是论文引用关系的图,节点是论文,边是是否有引用关系。然后每个节点都用词袋模型做了个1433维的特征。这些论文可以分为7种,给定70%的节点作为训练集,我们的任务就是预测测试集中每篇论文的类别。具体的完整代码看https://github.com/FighterLYL/GraphNeuralNetwork/tree/master/chapter5就好