On this page

    Getting started

    Spektral 是根据 Keras 的指导原则设计的,对初学者来说非常简单,同时对专家来说保持灵活性。

    在本教程中,我们将介绍spectral的主要函数,同时创建一个用于图分类的图神经网络。

    Graphs

    图是表示实体之间关系的数学对象。我们称实体为“节点”,称关系为“边”。

    节点和边都可以有向量特征。

    在Spektral中,图形用Spektral .data. graph实例来表示。一个图可以有四个主要属性。

    一个图可以拥有所有这些属性,也可以一个都没有。由于 Graphs 只是普通的Python对象,如果需要,还可以添加额外的属性。例如 graph.n_nodes, graph.n_node_features 等等。

    Adjacency matrix (graph.a)

    如果存在从节点 i 到节点 j 的边,则邻接矩阵的每一项 a[i, j] 都是非零的,否则为零。

    我们可以将 a 表示为一个稠密的 np.array 或者一个形状为 [n_nodes, n_nodes] 的 Scipy 稀疏矩阵。 使用一个 np.array 表示邻接矩阵是昂贵的, 因为我们需要在内存中存储大量的 0s, 因此通常更喜欢使用稀疏矩阵。

    稀疏矩阵,我们只需要存储 a 的非零项。在实践中, 我们可以通过只存储实现稀疏矩阵的索引和非零值在一个列表中, 并假设如果列表中一对索引缺失, 则对应的值是0。这被称为 COOrdinate format,TensorFlow使用这种格式来表示稀疏张量。

    例如,一个有4个节点的加权环图的邻接矩阵:

    [[0, 1, 0, 2],
     [3, 0, 4, 0],
     [0, 5, 0, 6],
     [7, 0, 8, 0]]
    

    可以用 COOrdinate format 表示如下:

    R, C, V
    0, 1, 1
    0, 3, 2
    1, 0, 3
    1, 2, 4
    2, 1, 5
    2, 3, 6
    3, 0, 7
    3, 2, 8
    

    其中 R 表示行,C 表示列,V 表示非零值 a[i, j] 。例如,在第二行中,我们看到有一条从节点0到节点3的边,权值为2。

    我们还看到,在这种情况下,所有的边都有一条对应的,方向相反的边。在这个例子中,所有的边都被赋予了不同的权重。然而在实际中,边 i, j 通常和边 j, i 有相同的权值,并且邻接矩阵是对称的。

    Spektral中的许多卷积和池化层使用矩阵的稀疏表示来进行计算,有时你会在文档中看到这样的注释:“这一层期望一个稀疏邻接矩阵。

    Node features (graph.x)

    当使用图神经网络时,我们通常将一个特征向量与图的每个节点相关联。这与图像中的每个像素都有一个[R, G, B, A] 向量是一样的。

    由于我们有 n_nodes 个节点,每个节点都有一个大小为 n_node_features 个节点特征的特征向量,我们可以将所有特征叠加在一个形状为 [n_nodes,n_nodes_features] 的矩阵 x 中。

    在Spektral中,x 总是用一个稠密的 np.array表示(因为在这种情况下,我们不需要冒存储许多无用的零的风险——至少不经常)。

    Edge features (graph.e)

    与节点特征相似,我们也可以有与边相关的特征。这些通常不同于我们看到的邻接矩阵的边权值,通常表示两个节点之间的关系(例如,熟人、朋友或伙伴)。

    在表示边特征时,我们遇到了与邻接矩阵相同的问题。

    如果我们把它们存储在稠密的 np.array 中,那么数组将具有形状 [n_nodes,n_ndoes,n_edge_features],并且它的大多数条目将是0。不幸的是,3阶张量不能用Scipy稀疏矩阵表示,所以我们需要聪明一点。

    类似于我们将邻接矩阵存储为 r, c, v 的列表,这里我们可以使用 COOrdinate format 来表示我们的边特征。假设,在上面的例子中,每条边有 n_edge_features=3 个特征。我们可以这样做:

    R, C, V
    0, 1, [ef_1, ef_2, ef_3]
    0, 3, [ef_1, ef_2, ef_3]
    1, 0, [ef_1, ef_2, ef_3]
    1, 2, [ef_1, ef_2, ef_3]
    2, 1, [ef_1, ef_2, ef_3]
    2, 3, [ef_1, ef_2, ef_3]
    3, 0, [ef_1, ef_2, ef_3]
    3, 2, [ef_1, ef_2, ef_3]
    

    由于我们在邻接矩阵中已经有了 RC 的信息,我们只需要将 V 列存储为一个形状为 [n_nodes,n_edge_feature] 的矩阵 e。在这种情况下,n条边表示邻接矩阵中非零项的个数。

    注意,由于我们已经将边特征从邻接矩阵的边索引中分离出来,所以我们存储边特征的顺序非常重要。我们不能破坏 a 中的边和 e 中的边之间的对应关系。

    在 Spektra l中,我们总是假设边是以行为主排序的(我们首先按行排序,然后按列排序,就像上面的例子一样)。这在构造邻接矩阵时不重要,但在构造 e 时很重要。

    你可以使用 spektral.utils.sparse.reorder 来根据边索引给出的正确的行主顺序对边缘特征矩阵进行排序(即,通过 steacking RC 列得到的矩阵)。

    Labels (graph.y)

    最后,在许多机器学习任务中,我们希望在给定输入的情况下预测一个标签。使用 GNN 时,标签可以有两种类型:

    Spektral 同时支持两种。

    标签是稠密的 np.array 或 scalars,存储在 Graph 对象的 y 属性中。 Graph-level 标签可以是标量,也可以是形状为 [n_labels,] 的一维数组。Node-level 标签可以是形状为 [n_nodes,]的一维数组(表示每个节点的标量标签),也可以是形状为 [n_nodes,n_labels] 的二维数组。

    这种差异只在使用 DisjointLoader 时才有意义。

    Dataset

    spektral.data.Dataset 容器提供了一些操作 graphs 集合的有用函数。

    让我们加载一个用于图分类的流行基准数据集

    >>> from spektral.datasets import TUDataset
    
    >>> dataset = TUDataset('PROTEINS')
    
    >>> dataset
    TUDataset(n_graphs=1113)
    

    现在我们可以检索单个图

    >>> dataset[0]
    Graph(n_nodes=42, n_node_features=4, n_edge_features=None, y=[1. 0.])
    

    或者打乱数据:

    >>> np.random.shuffle(dataset)
    

    或者将 dataset 切片为 sub-datasets:

    >>> dataset[:100]
    TUDataset(n_graphs=100)
    

    数据集还提供了将转换应用到每个数据的方法:

    例如,让我们修改数据集,使我们的图只有少于500个节点

    >>> dataset.filter(lambda g: g.n_nodes < 500)
    
    >>> dataset
    TUDataset(n_graphs=1111)  # removed 2 graphs
    

    现在我们对图像进行一些变换。例如,我们可以修改每个图,使节点特征也包含节点的独热编码的度。

    首先,我们计算数据集的最大的度,这样我们就知道独热向量的大小。

    >>> max_degree = dataset.map(lambda g: g.a.sum(-1).max(), reduce=max)
    
    >>> max_degree
    12
    

    试着检查lambda函数,看看它是做什么的。另外,请注意,我们向该方法传递了一个 reduce 函数。这将在计算的输出列表上运行 map。

    现在我们准备用独热编码度来增强我们的节点特征。Spektral有很多我们可以使用的预实现的 transforms

    from spektral.transforms import Degree
    dataset.apply(Degree(int(max_degree)))
    

    我们可以看到它是有效的,因为现在我们有了额外的 max_degree + 1 节点特征。

    >>> dataset[0]
    Graph(n_nodes=42, n_node_features=17, n_edge_features=None, y=[1. 0.])
    

    由于我们将在我们的 GNN 中使用 GCNConv 层,我们也想遵循引入这一层的原始论文,并对邻接矩阵做一些额外的预处理。

    因为这是一个相当常见的操作,所以 Spektral 有一个 transform 来完成它:

    >>> from spektral.transforms import GCNFilter
    
    >>> dataset.apply(GCNFilter())
    

    许多层都需要进行某种形式的预处理。如果你不想每次都回到文献中,Spektral 中的每一个卷积层都有一个 preprocess(a) 方法,你可以根据需要使用它来变换邻接矩阵。

    Creating a GNN

    创建 GNNs 是 Spektral 真正的亮点。由于 Spektral 被设计为 Keras 的扩展,你可以将任何 Spektral 层插入 Keras Model 而无需修改。

    我们只需要使用 functional API,因为 GNN 层通常需要两个或更多的输入(所以现在没有 Sequential 模型)。

    对于我们的第一个GNN,我们将创建一个简单的网络,首先进行一些图卷积,然后将所有节点汇总在一起(称为“global pooling”),最后用一个密集的 softmax 层对结果进行分类。我们也会用 dropout 来进行正则化。

    让我们从导入必要的层开始:

    from tensorflow.keras.models import Model
    from tensorflow.keras.layers import Dense, Dropout
    from spektral.layers import GCNConv, GlobalSumPool
    

    现在我们可以使用模型子类化来定义我们的模型:

    class MyFirstGNN(Model):
    
        def __init__(self, n_hidden, n_labels):
            super().__init__()
            self.graph_conv = GCNConv(n_hidden)
            self.pool = GlobalSumPool()
            self.dropout = Dropout(0.5)
            self.dense = Dense(n_labels, 'softmax')
    
        def call(self, inputs):
            out = self.graph_conv(inputs)
            out = self.dropout(out)
            out = self.pool(out)
            out = self.dense(out)
    
            return out
    

    注意我们是如何将 Spektral 和 Keras 的层混合在一起的: 这一切都是基于 tensors underneath 进行计算。

    这也意味着,你可以摆脱 GraphDataset 以及 Spektral 的其他特性。

    注意:如果您不想子类化 Model 来实现 GNN,您也可以使用经典的声明风格。您只需要注意 Input,并保留未指定的“节点”维度(因此使用 None 而不是 n_nodes)。

    Training the GNN

    现在我们可以训练 GNN 了。首先,我们实例化并编译我们的模型:

    model = MyFirstGNN(32, dataset.n_labels)
    model.compile('adam', 'categorical_crossentropy')
    

    然而,这就是 graph 阻碍我们的地方。与图像或序列等常规数据不同,图不能被拉伸、剪切或重塑,以便我们将它们放入预定义形状的张量中。如果一个图有10个节点,而另一个有4个节点,我们必须保持这种状态。

    这意味着对一个数据集进行小批量的迭代并不是 trivial 的,我们不能简单地使用 Keras 的 model.fit() 方法。

    Loaders

    Loaders 遍历一个图数据集以创建 mini-batches。它们在流程背后隐藏了很多复杂性,这样您就不需要考虑它了。您只需要访问 这个页面 并阅读 data modes,以便知道使用哪个加载器。

    每个 loader 都有一个 load() 方法,该方法返回一个 Keras 可以处理的数据生成器。因为我们正在进行 graph-level 分类,所以可以使用 BatchLoader 。它有点慢而且占用内存( DisjointLoader 会更好),但它让我们简化了 MyFirstGNN 的定义。同样,在本教程之后,请阅读有关 data modes 的内容。

    from spektral.data import BatchLoader
    
    loader = BatchLoader(dataset_train, batch_size=32)
    

    由于 loaders 本质上是生成器,我们需要为 model.fit() 提供每个 steps_per_epoch,而不需要指定 batch size

    model.fit(loader.load(), steps_per_epoch=loader.steps_per_epoch, epochs=10)
    

    Evaluating the GNN

    评估我们模型的性能,无论是测试还是验证,都遵循类似的工作流程。

    我们创建一个数据加载器

    from spektral.data import BatchLoader
    
    loader = BatchLoader(dataset_test, batch_size=32)
    

    并通过调用 load() 将其提供给模型

    loss = model.evaluate(loader.load(), steps=loader.steps_per_epoch)
    
    print('Test loss: {}'.format(loss))
    

    Node-level learning

    除了学习预测整个图的标签(如本教程所述),GNNs 在学习预测每个节点的标签方面非常有效。这被称为“node-level learning”,我们通常对带有一个大图的数据集(比如社交网络)进行这种学习。

    例如,可以使用 GCNConv 层、Citation 数据集和 SingleLoader 来复现 GCN 论文的结果,以对引文网络中的节点进行分类: 查看这个示例

    Go create!

    现在可以使用 Spektral 创建自己的 GNNs 了。

    如果您希望为特定任务构建GNN,那么Spektral中可能已经包含了所需的所有内容。看看这些例子,了解一些想法和实用技巧。

    请记住阅读 data modes 一节,了解如何表示 graphs 和创建 mini-batches。