第三章 无监督图学习3 - 图神经网络 Graph neural networks
文章目录
前言
Graph neural network,GNN 是处理图形结构数据的深度学习方法。这类方法也被称为几何深度学习 geometric deep learning ,并在各种应用中引起了广泛的兴趣,包括社会网络分析和计算机图形学。
根据第二章图机器学习中定义的分类,编码器部分以图结构和节点特征作为输入。这些算法可以在有或没有监督的情况下进行训练。在本章中,我们将专注于无监督训练,而监督设置将在第四章,监督图学习中探讨。
3.4 图神经网络 Graph neural networks
如果您熟悉卷积神经网络(CNN)的概念,您可能已经知道它们能够在处理常规欧几里德空间(如文本(一维)、图像(二维)和视频(三维))时获得令人印象深刻的结果。经典的CNN由层序列组成,每一层提取多尺度的局部空间特征。这些特征被更深层的层利用来构造更复杂和高度表达的表示。
近年来,人们发现诸如多层和局部性等概念对于处理图结构数据也很有用。然而,图是在一个非欧几里德空间上定义的,要为图找到一个CNN的泛化并不简单,如图3.20所示:
GNN的最初的构想是由Scarselli等人在2009年提出的。它依赖于一个事实,即每个节点都可以用它的特征和它的邻域来描述。来自邻域(表示图域中的局部性概念)的信息可以聚合并用于计算更复杂和高级的特征。让我们更详细地了解它是如何实现的。
一开始,每个节点 v i v_i vi都与一个状态相关联。让我们从一个随机嵌入 h i t ℎ_i^t hit开始(为了简单起见,忽略节点属性)。在算法的每次迭代中,节点使用一个简单的神经网络层积累来自其邻居的输入:
h i t = ∑ v j ∈ N ( v i ) σ ( W h j t − 1 + b ) h_i^t = \sum_{v_j \in N(v_i)} \sigma(Wh_j^{t-1}+b) hit=vj∈N(vi)∑σ(Whjt−1+b)
其中, W ∈ R d × d W \in \mathbb{R}^{d \times d} W∈Rd×d和 b ∈ R d b \in \mathbb{R}^{d} b∈Rd是可训练参数(其中d为嵌入的维数), σ \sigma σ是非线性函数, t t t表示算法的第 t t t次迭代。这个方程可以递归地应用,直到达到一个特定的目标。请注意,在每次迭代中,之前的状态(在之前的迭代中计算的状态)被用来计算新的状态已经在循环神经网络recurrent neural networks 中得到了应用。
3.4.1 GNNs的其他形式
从GNN最初的构想开始,近年来已经进行了一些尝试,以重新解决从图数据中学习的问题。特别是,前面描述的GNN的变体已经被提出,目的是提高其表示学习能力。其中一些专门设计用于处理特定类型的图(有向、无向、加权、未加权、静态、动态等等)。
此外,还对传播步提出了一些改进(卷积、门机制、注意机制和跳跃连接等),目的是在不同级别上改善表示。此外,还提出了不同的训练方法来改善学习。
在处理无监督表示学习时,最常见的方法之一是使用编码器来嵌入图(编码器被表示为GNN变体之一),然后使用一个简单的解码器重构邻接矩阵。
损失函数通常表示为原始邻接矩阵与重构邻接矩阵之间的相似度。形式上,它可以定义为:
Z = G N N ( X , A ) , s.t. A ^ = Z Z T Z = GNN(X, A), \text{s.t.} \hat{A} = ZZ^T Z=GNN(X,A),s.t.A^=ZZT
其中, A ∈ R N × N A \in \mathbb{R}^{N \times N} A∈RN×N为邻接矩阵表示, X ∈ R N × d X \in \mathbb{R}^{N \times d} X∈RN×d为节点属性矩阵。
这种方法的另一种常见变体,特别是在处理图分类/表示学习时,是针对目标距离 进行训练。其思想是同时嵌入两对图,获得一个组合表示。然后对模型进行训练,使其表示与距离匹配。在使用节点相似度函数处理节点分类/表示学习时,也可以采用类似的策略。
基于图卷积神经网络(Graph Convolutional Neural Network, GCN) 的编码器是用于无监督学习的最广泛的GNN变体之一。GCN是受CNN背后许多基本思想启发的GNN模型。滤波器参数通常在图中的所有位置上共享,几个层被连接起来形成一个深层网络。
图数据的卷积操作本质上有两种类型,即谱spectral 方法和非谱(空间)non-spectral (spatial) 方法。顾名思义,第一个定义了谱域的卷积(即,将图分解为简单元素的组合)。空间卷积将卷积定义为聚集来自邻居的特征信息。
3.4.2 谱卷积 Spectral graph convolution
谱方法与谱图理论相关,研究与图相关的特征多项式、特征值和矩阵的特征向量有关的图的特征。卷积运算定义为信号(节点特征)与核的乘积。更详细地说,它定义在通过确定图拉普拉斯矩阵的特征分解来实现的傅里叶域上(把图拉普拉斯矩阵想象成一个以特殊方式归一化的邻接矩阵)。
虽然这种谱卷积的定义有很强的数学基础,但该操作的计算代价昂贵。基于这个原因,已经的一些工作试图以一种有效的方法来近似它。例如,Defferrard等人的ChebNet是关于谱图卷积的第一个开创性工作之一。这里,通过使用K阶切比雪夫多项式(一种用于有效逼近函数的特殊多项式)的概念来逼近操作。
这里,K是一个非常有用的参数,因为它决定了滤波器的位置。直观地说,对于K=1,只有节点特征被输入到网络中。当K=2时,将two-hop邻节点(邻节点的邻节点)的平均作为输入,以此类推。
设 X ∈ R N × d X \in \mathbb{R}^{N \times d} X∈RN×d为节点特征矩阵。在经典的神经网络处理中,该信号将由以下形式的层组成:
H l = σ ( X W ) H^l = \sigma (XW) Hl=σ(XW)
W ∈ R N × N W \in \mathbb{R}^{N \times N} W∈RN×N是层权重值, σ \sigma σ 表示非线性激活函数。这种操作的缺点是,它独立处理每个节点的信号,而不考虑节点之间的连接。为了克服这个限制,可以做一个简单(但有效)的修改,如下所示:
H l = σ ( A X W ) H^l = \sigma (AXW) Hl=σ(AXW)
通过引入邻接矩阵 A ∈ R N × N A \in \mathbb{R}^{N \times N} A∈RN×N, 每个节点与其对应的邻节点之间增加了一个新的线性组合。这样,信息只依赖于邻域,但参数将应用于所有节点。
值得注意的是,该操作可以按顺序重复多次,从而创建一个深度网络。每一层中节点描述符 X X X将被前一层的输出 H l − 1 H^{l-1} Hl−1替换。
然而,前面提出的公式有一些局限性,不能直接应用。第一个限制是,通过乘以 A A A,我们考虑的是节点的所有邻居,而不是节点本身。这个问题可以很容易地通过在图中添加自循环来克服,即 A ^ = A + I \hat{A} = A+I A^=A+I。
第二个限制与邻接矩阵本身有关。由于它通常是不归一化的,我们会观察到high-degree节点的特征表示值大,而low-degree节点的特征表示值小。这将导致在训练过程中会出现一些问题,因为优化算法往往对特征的大小敏感。解决方案是对A进行归一化。
例如,在Kipf和Welling, 2017(著名的GCN模型之一)中,通过将 A A A乘以对角线节点度矩阵diagonal node degree matrix D D D D − 1 A D^{-1}A D−1A来进行归一化,使得所有行的和为1。更具体地说,使用对称标准化 D − 1 / 2 A D − 1 / 2 D^{-1/2}AD^{-1/2} D−1/2AD−1/2,使得提出的传播规则为:
H
l
=
σ
(
D
^
−
1
/
2
A
D
^
−
1
/
2
X
W
)
H^l = \sigma (\hat{D}^{-1/2}A\hat{D}^{-1/2}XW)
Hl=σ(D^−1/2AD^−1/2XW)
其中
D
^
\hat{D}
D^是
A
^
\hat{A}
A^的对角度矩阵。
在下面的例子中,我们将创建一个Kipf和Welling中定义的GCN,我们将应用这个传播规则来嵌入一个著名的网络:Zachary’s karate club graph。
# 利用networkx加载哑铃图
#from networkx import karate_club_graph, to_numpy_matrix
import numpy as np
import networkx as nx
from scipy.linalg import sqrtm
import matplotlib.pyplot as plt
G = nx.barbell_graph(m1=10, m2=4)
#为了实现GraphConv传播规则,我们需要一个邻接矩阵A来表示G.由于该网络不具有节点特征,我们将使用单位矩阵作为节点描述符
order = np.arange(G.number_of_nodes())
A = nx.to_numpy_matrix(G, nodelist=order)
I = np.eye(G.number_of_nodes())
np.random.seed(7)
A_hat = A + np.eye(G.number_of_nodes()) # add self-connections
D_hat = np.array(np.sum(A_hat, axis=0))[0] # diagonal node degree matrix:
D_hat = np.array(np.diag(D_hat))
D_hat = np.linalg.inv(sqrtm(D_hat))
A_hat = D_hat @ A_hat @ D_hat
# Layer weights W will be initialized using Glorot uniform initialization
# other initialization methods can be also used, e.g., Gaussian or uniform
def glorot_init(nin, nout):
sd = np.sqrt(6.0 / (nin + nout))
return np.random.uniform(-sd, sd, size=(nin, nout))
# GCN propagation rule
class GCNLayer():
def __init__(self, n_inputs, n_outputs):
self.n_inputs = n_inputs
self.n_outputs = n_outputs
self.W = glorot_init(self.n_outputs, self.n_inputs)
self.activation = np.tanh
def forward(self, A, X):
self._X = (A @ X).T # (N,N)*(N,n_outputs) ==> (n_outputs,N)
H = self.W @ self._X # (N, D)*(D, n_outputs) => (N, n_outputs)
H = self.activation(H)
return H.T # (n_outputs, N)
# create network
gcn1 = GCNLayer(G.number_of_nodes(), 8)
gcn2 = GCNLayer(8, 4)
gcn3 = GCNLayer(4, 2) # 输出层输出数目为2,表示输出的嵌入为2维嵌入,方便可视化
# compute the forward pass
H1 = gcn1.forward(A_hat, I)
H2 = gcn2.forward(A_hat, H1)
H3 = gcn3.forward(A_hat, H2) #H3包含使用GCN传播规则计算的嵌入。
embeddings = H3
def draw_graph(G, filename=None, node_size=50):
pos_nodes = nx.spring_layout(G)
nx.draw(G, pos_nodes, with_labels=False, node_size=node_size, edge_color='gray')
pos_attrs = {}
for node, coords in pos_nodes.items():
pos_attrs[node] = (coords[0], coords[1] + 0.08)
plt.axis('off')
axis = plt.gca()
axis.set_xlim([1.2*x for x in axis.get_xlim()])
axis.set_ylim([1.2*y for y in axis.get_ylim()])
embeddings = np.array(embeddings)
draw_graph(G)
plt.scatter(embeddings[:, 0], embeddings[:, 1])
plt.savefig('embedding_gcn.png',dpi=300)
GCN嵌入的结果可以很明显的观察到两个相当分离的部分。考虑到我们还没有训练这个网络,这是一个很好的结果!
谱图卷积方法在许多领域都取得了显著的成果。然而,它们也存在一些缺点。例如,考虑一个具有数十亿个节点的非常大的图,谱方法要求同时处理图,从计算的角度来看这是不现实的。
此外,谱卷积通常假设一个固定的图,导致在新的不同的图上泛化能力较差。为了克服这些问题,空间图卷积是另一种有趣的尝试。
3.4.3 空间图卷积 Spectral graph convolution
空间图卷积网络通过聚合来自空间近邻的信息直接对图执行操作。空间卷积有很多优点:权值可以很容易地在图的不同位置共享,从而在不同的图上具有很好的泛化能力。此外,计算可以通过考虑节点的子集而不是整个图来完成,这可能会提高计算效率。
GraphSAGE是实现空间卷积的算法之一。其主要特点之一是能够在各种类型的网络上扩展。
GraphSAGE由三个步骤组成:
-
邻域采样 Neighborhood sampling :对于图中的每个节点,第一步是找到它的k邻域,其中k是由用户定义的,用来决定需要考虑多少hops(邻域的邻域)。
-
聚合 Aggregation :对于每个节点,描述各自邻域的节点特征。可以执行各种类型的聚合,包括平均聚合、池化聚合(例如,根据特定的标准采用最佳特性),或者更复杂的操作,例如使用循环单位(如LSTM)。
-
预测 Prediction:每个节点都配备了一个简单的神经网络,该网络学习如何基于来自邻域的聚合特征进行预测。
GraphSAGE经常用于监督学习。然而,通过采用诸如使用相似函数作为目标距离的策略,它也可以有效地进行学习嵌入,而无需明确地监督任务。
3.4.4 图卷积实战
在实际应用中,GNN已经在许多机器学习和深度学习框架中实现,包括TensorFlow、Keras和PyTorch。在下一个例子中,我们将使用用于图形机器学习的Python库StellarGraph
。
在下面的例子中,我们将学习在没有目标变量的情况下,以无监督的方式嵌入向量。该方法受到Bai et al. 2019的启发,基于同时嵌入成对图。该嵌入用来匹配图之间的ground-truth距离:
- 安装StellarGraph, 加载需要的Python模块。
# install StellarGraph
!pip install -q stellargraph[demos]==1.2.1
# load the required Python modules
import pandas as pd
import numpy as np
import networkx as nx
import os
import stellargraph as sg
from stellargraph.mapper import FullBatchNodeGenerator
from stellargraph.layer import GCN
import tensorflow as tf
from tensorflow.keras import layers, optimizers, losses, metrics, Model
from sklearn import preprocessing, model_selection
from IPython.display import display, HTML
import matplotlib.pyplot as plt
%matplotlib inline
- 加载数据集。我们将在这个例子中使用
PROTEINS
数据集,该数据集可从StellarGraph中获取,由1114个图组成,平均每个图有39个节点,73条边。每个节点由四个属性描述,属于两个类中的一个。
dataset = sg.datasets.PROTEINS()
display(HTML(dataset.description))
graphs, graph_labels = dataset.load()
Each graph represents a protein and graph labels represent whether they are are enzymes or non-enzymes. The dataset includes 1113 graphs with 39 nodes and 73 edges on average for each graph. Graph nodes have 4 attributes (including a one-hot encoding of their label), and each graph is labelled as belonging to 1 of 2 classes.
# let's print some info to better understand the dataset
print(graphs[0].info())
graph_labels.value_counts().to_frame()
StellarGraph: Undirected multigraph
Nodes: 42, Edges: 162
Node types:
default: [42]
Features: float32 vector, length 4
Edge types: default-default->default
Edge types:
default-default->default: [162]
Weights: all 1 (default)
Features: none
label
1 663
2 450
- 创建模型。它将由两个输出尺寸为64和32的图卷积层组成,然后分别接入ReLU激活层。输出为两个嵌入的欧几里德距离:
# TODO
generator = sg.mapper.PaddedGraphGenerator(graphs)
# define a GCN model containing 2 layers of size 64 and 32, respectively.
# ReLU activation function is used to add non-linearity between layers
gc_model = sg.layer.GCNSupervisedGraphClassification(
[64, 32], ["relu", "relu"], generator, pool_all_layers=True
)
inp1, out1 = gc_model.in_out_tensors()
inp2, out2 = gc_model.in_out_tensors()
vec_distance = tf.norm(out1 - out2, axis=1)
pair_model = Model(inp1 + inp2, vec_distance)
embedding_model = Model(inp1, out1)
- 准备训练。对于每一对输入图,我们将分配一个相似度分数。在这种情况下可以使用任何图形相似性的概念,包括图形编辑距离。为了简单起见,我们将使用图的拉普拉斯谱距离:
def graph_distance(graph1, graph2):
spec1 = nx.laplacian_spectrum(graph1.to_networkx(feature_attr=None))
spec2 = nx.laplacian_spectrum(graph2.to_networkx(feature_attr=None))
k = min(len(spec1), len(spec2))
return np.linalg.norm(spec1[:k] - spec2[:k])
graph_idx = np.random.RandomState(0).randint(len(graphs), size=(100, 2))
targets = [graph_distance(graphs[left], graphs[right]) for left, right in graph_idx]
train_gen = generator.flow(graph_idx, batch_size=10, targets=targets)
- 编译和训练模型。我们将使用自适应矩估计优化器(Adam),学习速率参数设置为1e-2。我们将使用的损失函数定义为预测和ground-truth距离之间的最小平方误差。模型将500次迭代训练
pair_model.compile(optimizers.Adam(1e-2), loss="mse")
history = pair_model.fit(train_gen, epochs=500, verbose=0)
sg.utils.plot_history(history)
6. 检查和可视化结果。
在训练之后可以检查和可视化学习到的表示。由于输出是32维的,我们需要一种方法来定性地评估嵌入,例如,通过在二维空间中绘制它们。我们将使用T-SNE。把嵌入图画出来。在图中,每个点(嵌入图)都根据对应的标签(蓝色=0,红色=1)着色。结果如图所示。
以上只是学习图形嵌入的一种可能的方法。可以尝试更先进的解决方案,以更好地适应感兴趣的问题。
embeddings = embedding_model.predict(generator.flow(graphs))
from sklearn.manifold import TSNE
tsne = TSNE(2)
two_d = tsne.fit_transform(embeddings)
plt.scatter(two_d[:, 0], two_d[:, 1], c=graph_labels.cat.codes, cmap="jet", alpha=0.4)
plt.savefig('embedding_TSNE.png',dpi=300)
总结
在本章中,我们学习了如何将无监督机器学习有效地应用于图来解决实际问题,如节点和图表示学习。
我们首先分析了浅嵌入方法,这是一组能够学习并只返回学习到的输入数据的嵌入值的算法。
然后,我们学习了如何使用自动编码器算法,通过在低维空间中保存重要信息来编码输入。通过学习允许我们重构成对节点/图相似性的嵌入,我们还了解了这种思想如何适用于图。
最后,我们介绍了GNN背后的主要概念。并介绍如何将卷积等概念应用到图中。
在下一章中,我们将在有监督的环境中修改这些概念。监督学习会提供一个目标标签,目的是学习输入和输出之间的映射。